diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index bd4c558f..15350519 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -5,16 +5,17 @@ on: [push] jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: + os: [ubuntu-latest, windows-latest] python-version: ['3.8', '3.9', '3.10'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -32,11 +33,11 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Run tests and generate coverage data run: | - python3 -m pip install coverage pytest + python3 -m pip install coverage pytest pyfakefs coverage run -m pytest - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: flags: unittests name: codecov-umbrella diff --git a/.gitignore b/.gitignore index faf6c81e..2c1c3eb5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,11 +12,14 @@ build *.egg-info /examples/log -/src/log -/src/tests/log +/log +/tests/log +/tmtccmd/log .idea venv +/misc/.xdp-* + # CodeCov .coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index b6756dce..392734bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ## [unreleased] +## [v3.0.0rc1] 03.07.2022 + +- Overhaul of application architecture +- Significant simplification of various modules to increase testability +- Test coverage increased +- Reduced number of modules significantly by moving code into the repective `__init__` files where + possible +- GUI improved, added separate TM listening button +- Documentation improved +- New logo +- Simplified general package structure, remove `src` folder and have `tmtccmd` package and `tests` + package in repo root +- First CFDP handler components +- Reduce usage of globals. The end goal is to remove them altogether +- Reduce overall number of spawned threads +- Added Sequence Count handling modules + ## [v2.2.2] - Improve internal structure of sequential sender receiver object diff --git a/MANIFEST.in b/MANIFEST.in index 2c321ebd..2e311a7d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,12 +1,15 @@ include LICENSE include NOTICE +include CHANGELOG.md -include docs/*.rst -include docs/logo_tmtccmd.png -include docs/conf.py -include docs/Makefile -include docs/*.md +graft docs +prune docs/_build -include tests/* +graft tests +prune tests/log -include pycharm/* \ No newline at end of file +include requirements.txt + +recursive-exclude * *.pyc *.pyo + +include misc/logo-tiny.png diff --git a/README.md b/README.md index 84a23be4..be022783 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -

+

-TMTC Commander Core [![Documentation Status](https://readthedocs.org/projects/tmtccmd/badge/?version=latest)](https://tmtccmd.readthedocs.io/en/latest/?badge=latest) +TMTC Commander [![Documentation Status](https://readthedocs.org/projects/tmtccmd/badge/?version=latest)](https://tmtccmd.readthedocs.io/en/latest/?badge=latest) [![package](https://github.com/spacefisch/tmtccmd/actions/workflows/package.yml/badge.svg)](https://github.com/spacefisch/tmtccmd/actions/workflows/package.yml) [![codecov](https://codecov.io/gh/robamu-org/tmtccmd/branch/develop/graph/badge.svg?token=BVOE3A4WE4)](https://codecov.io/gh/robamu-org/tmtccmd) [![PyPI version](https://badge.fury.io/py/tmtccmd.svg)](https://badge.fury.io/py/tmtccmd) @@ -8,43 +8,63 @@ TMTC Commander Core [![Documentation Status](https://readthedocs.org/projects/tm ## Overview -- Documentation: https://tmtccmd.readthedocs.io/en/latest/ -- Project Homepage: https://github.com/robamu-org/tmtccmd +- [Documentation](https://tmtccmd.readthedocs.io/en/latest/) +- [Project Homepage](https://github.com/robamu-org/tmtccmd) -This commander application was first developed by KSat for the -[SOURCE](https://www.ksat-stuttgart.de/en/our-missions/source/) project to test the on-board -software but has evolved into a more generic tool for satellite developers to perform TMTC -(Telemetry and Telecommand) handling and testing via different communication interfaces. -Currently, only the PUS standard is implemented as a packet standard. This tool can be used either -as a command line tool or as a GUI tool. The GUI features require a PyQt5 installation. +This is a small Python framework for satellite developers to perform TMTC +(Telemetry and Telecommand) handling and testing via different communication interfaces. +This tool can be used either as a command line tool or as a GUI tool. The GUI features require a +PyQt5 installation. This package also has dedicated support to send and receive ECSS PUS packets +or other generic CCSDS packets. -This client currently supports the following communication interfaces: +The TMTC commander also includes some telemetry handling components and telecommand packaging +helpers. Some of those components are tailored towards usage with the +[Flight Software Framework (FSFW)](https://egit.irs.uni-stuttgart.de/fsfw/fsfw/). + +## Features + +- Special support for `Packet Utilisation Standard (PUS)`_ packets and `CCSDS Space Packets`_. + This library uses the `spacepackets`_ library for most packet implementations. +- Support for both CLI and GUI usage +- Flexibility in the way to specify telecommands to send and how to handle incoming telemetry. + This is done by requiring the user to specify callbacks for both TC specification and TM handling. +- One-Queue Mode for simple command sequences and Multi-Queue for more complex command sequences +- Listener mode to only listen to incoming telemetry +- Basic logger components which can be used to store sent Telecommands and incoming Telemetry + in files +- Some components are tailored towards usage with the + `Flight Software Framework (FSFW) `_. + +This has a communication interface abstraction which allows to exchange TMTC through different +channels. The framework currently supports the following communication interfaces: 1. TCP/IP with UDP and TCP 2. Serial Communication using fixed frames or a simple ASCII based transport layer 3. QEMU, using a virtual serial interface -The TMTC commander also includes some telemetry handling components and telecommand packaging -helpers. Some of those components are tailored towards usage with the -[Flight Software Framework (FSFW)](https://egit.irs.uni-stuttgart.de/fsfw/fsfw/). +It is also possible to supply custom interfaces. ## Examples -The `example` folder contains a simple example using a dummy communication interface. -It can be run like this on Linux +The [`examples`](https://github.com/robamu-org/tmtccmd/tree/main/examples) folder contains a simple +example using a dummy communication interface. It sends a PUS ping telecommand and then reads the +ping reply and the verification replies back from the dummy interface. It can be run like this +on Linux: ```sh -cd example -./tmtccli.py +cd examples +./tmtcc.py ``` -or on Windows +or on Windows: ```sh -cd example -py tmtccli.py +cd examples +py tmtcc.py ``` +You can run the GUI mode by supplying `-g` to the commands above. + The [EIVE](https://egit.irs.uni-stuttgart.de/eive/eive-tmtc) and [SOURCE](https://git.ksat-stuttgart.de/source/tmtc) project implementation of the TMTC commander provide more complex implementations. diff --git a/docs/README_PyPI.md b/docs/README_PyPI.md index 8d9aa68b..6c1e2185 100644 --- a/docs/README_PyPI.md +++ b/docs/README_PyPI.md @@ -1,6 +1,6 @@ -![](https://github.com/rmspacefish/tmtccmd/blob/source/docs/logo_tmtccmd_smaller.png) +![](https://github.com/robamu-org/tmtccmd/blob/master/misc/logo.png) -TMTC Commander Core [![Documentation Status](https://readthedocs.org/projects/tmtccmd/badge/?version=latest)](https://tmtccmd.readthedocs.io/en/latest/?badge=latest) +TMTC Commander[![Documentation Status](https://readthedocs.org/projects/tmtccmd/badge/?version=latest)](https://tmtccmd.readthedocs.io/en/latest/?badge=latest) [![package](https://github.com/spacefisch/tmtccmd/actions/workflows/package.yml/badge.svg)](https://github.com/spacefisch/tmtccmd/actions/workflows/package.yml) [![codecov](https://codecov.io/gh/robamu-org/tmtccmd/branch/develop/graph/badge.svg?token=BVOE3A4WE4)](https://codecov.io/gh/robamu-org/tmtccmd) [![PyPI version](https://badge.fury.io/py/tmtccmd.svg)](https://badge.fury.io/py/tmtccmd) @@ -10,4 +10,3 @@ TMTC Commander Core [![Documentation Status](https://readthedocs.org/projects/tm - Project Homepage: https://github.com/robamu-org/tmtccmd - Documentation: https://tmtccmd.readthedocs.io/en/latest/ - diff --git a/docs/api.rst b/docs/api.rst index fda03001..2e5cf3b9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -5,10 +5,10 @@ API Core Submodules =============== -tmtccmd.runner module +tmtccmd module --------------------- -.. automodule:: tmtccmd.runner +.. automodule:: tmtccmd :members: :undoc-members: :show-inheritance: @@ -17,7 +17,6 @@ tmtccmd.runner module :maxdepth: 4 api/tmtccmd.core - api/tmtccmd.sendreceive Configuration Submodules ========================= @@ -34,11 +33,10 @@ CCSDS & ECSS Submodules .. toctree:: :maxdepth: 4 - api/tmtccmd.ccsds + api/tmtccmd.cfdp api/tmtccmd.pus api/tmtccmd.tc api/tmtccmd.tm - api/tmtccmd.cfdp Other Submodules ========================= @@ -47,4 +45,5 @@ Other Submodules :maxdepth: 4 api/tmtccmd.utility - api/tmtccmd.logging \ No newline at end of file + api/tmtccmd.logging + api/tmtccmd.fsfw diff --git a/docs/api/tmtccmd.ccsds.rst b/docs/api/tmtccmd.ccsds.rst deleted file mode 100644 index 0fe7c06c..00000000 --- a/docs/api/tmtccmd.ccsds.rst +++ /dev/null @@ -1,21 +0,0 @@ -tmtccmd.ccsds package -===================== - -Submodules ----------- - -tmtccmd.ccsds.handler module --------------------------------- - -.. automodule:: tmtccmd.ccsds.handler - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: tmtccmd.ccsds - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/tmtccmd.com_if.rst b/docs/api/tmtccmd.com_if.rst index 9dca814c..19126315 100644 --- a/docs/api/tmtccmd.com_if.rst +++ b/docs/api/tmtccmd.com_if.rst @@ -1,85 +1,77 @@ tmtccmd.com\_if package ======================= +Module contents +--------------- + +.. automodule:: tmtccmd.com_if + :members: + :undoc-members: + :show-inheritance: + Submodules ---------- -tmtccmd.com\_if.com\_if\_utilities module +tmtccmd.com\_if.utils module ----------------------------------------- -.. automodule:: tmtccmd.com_if.com_if_utilities +.. automodule:: tmtccmd.com_if.utils :members: :undoc-members: :show-inheritance: -tmtccmd.com\_if.com\_interface\_base module +tmtccmd.com\_if.tcp module ------------------------------------------- -.. automodule:: tmtccmd.com_if.com_interface_base +.. automodule:: tmtccmd.com_if.tcp :members: :undoc-members: :show-inheritance: -tmtccmd.com\_if.dummy\_com\_if module +tmtccmd.com\_if.udp module ------------------------------------- -.. automodule:: tmtccmd.com_if.dummy_com_if +.. automodule:: tmtccmd.com_if.udp :members: :undoc-members: :show-inheritance: -tmtccmd.com\_if.qemu\_com\_if module ------------------------------------- +tmtccmd.com\_if.tcpip\_utils module +------------------------------------- -.. automodule:: tmtccmd.com_if.qemu_com_if +.. automodule:: tmtccmd.com_if.tcpip_utils :members: :undoc-members: :show-inheritance: -tmtccmd.com\_if.serial\_com\_if module --------------------------------------- +tmtccmd.com\_if.qemu module +------------------------------------ -.. automodule:: tmtccmd.com_if.serial_com_if +.. automodule:: tmtccmd.com_if.qemu :members: :undoc-members: :show-inheritance: -tmtccmd.com\_if.serial\_utilities module ----------------------------------------- +tmtccmd.com\_if.serial module +-------------------------------------- -.. automodule:: tmtccmd.com_if.serial_utilities +.. automodule:: tmtccmd.com_if.serial :members: :undoc-members: :show-inheritance: -tmtccmd.com\_if.tcpip\_tcp\_com\_if module ------------------------------------------- +tmtccmd.com\_if.ser\_utils module +---------------------------------------- -.. automodule:: tmtccmd.com_if.tcpip_tcp_com_if +.. automodule:: tmtccmd.com_if.ser_utils :members: :undoc-members: :show-inheritance: -tmtccmd.com\_if.tcpip\_udp\_com\_if module +tmtccmd.com\_if.dummy module ------------------------------------------ -.. automodule:: tmtccmd.com_if.tcpip_udp_com_if - :members: - :undoc-members: - :show-inheritance: - -tmtccmd.com\_if.tcpip\_utilities module ---------------------------------------- - -.. automodule:: tmtccmd.com_if.tcpip_utilities - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: tmtccmd.com_if +.. automodule:: tmtccmd.com_if.dummy :members: :undoc-members: :show-inheritance: diff --git a/docs/api/tmtccmd.config.rst b/docs/api/tmtccmd.config.rst index 1f61869e..c63fa369 100644 --- a/docs/api/tmtccmd.config.rst +++ b/docs/api/tmtccmd.config.rst @@ -1,17 +1,17 @@ tmtccmd.config package ====================== -Submodules ----------- - -tmtccmd.config.hook module --------------------------- +Module contents +--------------- -.. automodule:: tmtccmd.config.hook +.. automodule:: tmtccmd.config :members: :undoc-members: :show-inheritance: +Submodules +---------- + tmtccmd.config.args module -------------------------- @@ -20,18 +20,18 @@ tmtccmd.config.args module :undoc-members: :show-inheritance: -tmtccmd.config.com\_if module +tmtccmd.config.hook module ----------------------------- -.. automodule:: tmtccmd.config.com_if +.. automodule:: tmtccmd.config.hook :members: :undoc-members: :show-inheritance: -tmtccmd.config.definitions module ---------------------------------- +tmtccmd.config.com\_if module +----------------------------- -.. automodule:: tmtccmd.config.definitions +.. automodule:: tmtccmd.config.com_if :members: :undoc-members: :show-inheritance: @@ -51,11 +51,3 @@ tmtccmd.config.objects module :members: :undoc-members: :show-inheritance: - -Module contents ---------------- - -.. automodule:: tmtccmd.config - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/tmtccmd.core.rst b/docs/api/tmtccmd.core.rst index 3a7accbe..92d60547 100644 --- a/docs/api/tmtccmd.core.rst +++ b/docs/api/tmtccmd.core.rst @@ -1,29 +1,29 @@ tmtccmd.core package ==================== -Submodules ----------- - -tmtccmd.core.backend module ---------------------------- +Module contents +--------------- -.. automodule:: tmtccmd.core.backend +.. automodule:: tmtccmd.core :members: :undoc-members: :show-inheritance: -tmtccmd.core.frontend module ----------------------------- +Submodules +---------- + +tmtccmd.core.ccsds\_backend module +---------------------------------- -.. automodule:: tmtccmd.core.frontend +.. automodule:: tmtccmd.core.ccsds_backend :members: :undoc-members: :show-inheritance: -tmtccmd.core.frontend\_base module ----------------------------------- +tmtccmd.core.base module +---------------------------- -.. automodule:: tmtccmd.core.frontend_base +.. automodule:: tmtccmd.core.base :members: :undoc-members: :show-inheritance: @@ -35,19 +35,3 @@ tmtccmd.core.globals\_manager module :members: :undoc-members: :show-inheritance: - -tmtccmd.core.object\_id\_manager module ---------------------------------------- - -.. automodule:: tmtccmd.core.object_id_manager - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: tmtccmd.core - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/tmtccmd.fsfw.rst b/docs/api/tmtccmd.fsfw.rst new file mode 100644 index 00000000..884a56c7 --- /dev/null +++ b/docs/api/tmtccmd.fsfw.rst @@ -0,0 +1,13 @@ +tmtccmd.fsfw package +===================== + +Submodules +---------- + +Module contents +--------------- + +.. automodule:: tmtccmd.fsfw + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/tmtccmd.pus.rst b/docs/api/tmtccmd.pus.rst index deb55ecf..09dcfde3 100644 --- a/docs/api/tmtccmd.pus.rst +++ b/docs/api/tmtccmd.pus.rst @@ -4,38 +4,6 @@ tmtccmd.pus package Submodules ---------- -tmtccmd.pus.service\_17\_test module ------------------------------------- - -.. automodule:: tmtccmd.pus.service_17_test - :members: - :undoc-members: - :show-inheritance: - -tmtccmd.pus.service\_20\_parameter module ------------------------------------------ - -.. automodule:: tmtccmd.pus.service_20_parameter - :members: - :undoc-members: - :show-inheritance: - -tmtccmd.pus.service\_5\_event module ------------------------------------- - -.. automodule:: tmtccmd.pus.service_5_event - :members: - :undoc-members: - :show-inheritance: - -tmtccmd.pus.service\_list module --------------------------------- - -.. automodule:: tmtccmd.pus.service_list - :members: - :undoc-members: - :show-inheritance: - Module contents --------------- diff --git a/docs/api/tmtccmd.sendreceive.rst b/docs/api/tmtccmd.sendreceive.rst deleted file mode 100644 index 64214da8..00000000 --- a/docs/api/tmtccmd.sendreceive.rst +++ /dev/null @@ -1,53 +0,0 @@ -tmtccmd.sendreceive package -=========================== - -Submodules ----------- - -tmtccmd.sendreceive.cmd\_sender\_receiver module ------------------------------------------------- - -.. automodule:: tmtccmd.sendreceive.cmd_sender_receiver - :members: - :undoc-members: - :show-inheritance: - -tmtccmd.sendreceive.multiple\_cmds\_sender\_receiver module ------------------------------------------------------------ - -.. automodule:: tmtccmd.sendreceive.multiple_cmds_sender_receiver - :members: - :undoc-members: - :show-inheritance: - -tmtccmd.sendreceive.sequential\_sender\_receiver module -------------------------------------------------------- - -.. automodule:: tmtccmd.sendreceive.sequential_sender_receiver - :members: - :undoc-members: - :show-inheritance: - -tmtccmd.sendreceive.single\_command\_sender\_receiver module ------------------------------------------------------------- - -.. automodule:: tmtccmd.sendreceive.single_command_sender_receiver - :members: - :undoc-members: - :show-inheritance: - -tmtccmd.sendreceive.tm\_listener module ---------------------------------------- - -.. automodule:: tmtccmd.sendreceive.tm_listener - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: tmtccmd.sendreceive - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/tmtccmd.tc.rst b/docs/api/tmtccmd.tc.rst index 25d10fbc..e4f01c14 100644 --- a/docs/api/tmtccmd.tc.rst +++ b/docs/api/tmtccmd.tc.rst @@ -1,69 +1,85 @@ tmtccmd.tc package ======================= +Module contents +--------------- + +.. automodule:: tmtccmd.tc + :members: + :undoc-members: + :show-inheritance: + Submodules ---------- -tmtccmd.tc.definitions module ----------------------------------- +tmtccmd.tc.handler module +----------------------------- -.. automodule:: tmtccmd.tc.definitions +.. automodule:: tmtccmd.tc.handler :members: :undoc-members: :show-inheritance: -tmtccmd.tc.packer module +tmtccmd.tc.queue module ----------------------------- -.. automodule:: tmtccmd.tc.packer +.. automodule:: tmtccmd.tc.queue :members: :undoc-members: :show-inheritance: -tmtccmd.tc.service\_200\_mode module ------------------------------------------ +tmtccmd.tc.procedure module +----------------------------- -.. automodule:: tmtccmd.tc.service_200_mode +.. automodule:: tmtccmd.tc.procedure :members: :undoc-members: :show-inheritance: -tmtccmd.tc.service\_20\_parameter module ---------------------------------------------- +tmtccmd.tc.ccsds\_seq\_sender module +--------------------------------------- -.. automodule:: tmtccmd.tc.service_20_parameter +.. automodule:: tmtccmd.tc.ccsds_seq_sender :members: :undoc-members: :show-inheritance: -tmtccmd.tc.service\_3\_housekeeping module ------------------------------------------------ +tmtccmd.tc.pus\_200\_fsfw\_modes module +----------------------------------------- -.. automodule:: tmtccmd.tc.service_3_housekeeping +.. automodule:: tmtccmd.tc.pus_200_fsfw_modes :members: :undoc-members: :show-inheritance: -tmtccmd.tc.service\_5\_event module ----------------------------------------- +tmtccmd.tc.pus\_20\_params module +--------------------------------------------- -.. automodule:: tmtccmd.tc.service_5_event +.. automodule:: tmtccmd.tc.pus_20_params :members: :undoc-members: :show-inheritance: -tmtccmd.tc.service\_8\_functional\_cmd module --------------------------------------------------- +tmtccmd.tc.pus\_3\_fsfw\_hk module +----------------------------------------------- -.. automodule:: tmtccmd.tc.service_8_functional_cmd +.. automodule:: tmtccmd.tc.pus_3_fsfw_hk :members: :undoc-members: :show-inheritance: -Module contents ---------------- +tmtccmd.tc.pus\_5\_event module +---------------------------------------- -.. automodule:: tmtccmd.tc +.. automodule:: tmtccmd.tc.pus_5_event + :members: + :undoc-members: + :show-inheritance: + +tmtccmd.tc.pus\_8\_funccmd module +-------------------------------------------------- + +.. automodule:: tmtccmd.tc.pus_8_funccmd :members: :undoc-members: :show-inheritance: diff --git a/docs/api/tmtccmd.tm.rst b/docs/api/tmtccmd.tm.rst index b61d0b4f..3574ea9b 100644 --- a/docs/api/tmtccmd.tm.rst +++ b/docs/api/tmtccmd.tm.rst @@ -1,37 +1,45 @@ tmtccmd.tm package ======================= +Module contents +--------------- + +.. automodule:: tmtccmd.tm + :members: + :undoc-members: + :show-inheritance: + Submodules ---------- -tmtccmd.tm.definitions module ----------------------------------- +tmtccmd.tm.ccsds_tm_listener module +------------------------------------ -.. automodule:: tmtccmd.tm.definitions +.. automodule:: tmtccmd.tm.ccsds_tm_listener :members: :undoc-members: :show-inheritance: -tmtccmd.tm.handler module ----------------------------------- +tmtccmd.tm.pus\_1\_verification module +--------------------------------------- -.. automodule:: tmtccmd.tm.handler +.. automodule:: tmtccmd.tm.pus_1_verification :members: :undoc-members: :show-inheritance: -tmtccmd.tm.service\_200\_fsfw\_mode module --------------------------------------------- +tmtccmd.tm.pus\_5\_event module +---------------------------------------- -.. automodule:: tmtccmd.tm.service_200_fsfw_mode +.. automodule:: tmtccmd.tm.pus_5_event :members: :undoc-members: :show-inheritance: -tmtccmd.tm.service\_20\_fsfw\_parameters module ------------------------------------------------- +tmtccmd.tm.pus\_8\_funccmd module +----------------------------------------------------- -.. automodule:: tmtccmd.tm.service_20_fsfw_parameters +.. automodule:: tmtccmd.tm.pus_8_funccmd :members: :undoc-members: :show-inheritance: @@ -39,47 +47,47 @@ tmtccmd.tm.service\_20\_fsfw\_parameters module tmtccmd.tm.service\_2\_raw\_cmd module ------------------------------------------- -.. automodule:: tmtccmd.tm.service_2_raw_cmd +.. automodule:: tmtccmd.tm.pus_2_rawcmd :members: :undoc-members: :show-inheritance: -tmtccmd.tm.service\_3\_base module +tmtccmd.tm.pus\_3\_hk\_base module --------------------------------------- -.. automodule:: tmtccmd.tm.service_3_base +.. automodule:: tmtccmd.tm.pus_3_hk_base :members: :undoc-members: :show-inheritance: -tmtccmd.tm.service\_3\_fsfw\_housekeeping module +tmtccmd.tm.pus\_3\_fsfw\_hk module -------------------------------------------------- -.. automodule:: tmtccmd.tm.service_3_fsfw_housekeeping +.. automodule:: tmtccmd.tm.pus_3_fsfw_hk :members: :undoc-members: :show-inheritance: -tmtccmd.tm.service\_5\_event module ----------------------------------------- +tmtccmd.tm.pus\_20\_fsfw\_parameters module +------------------------------------------------ -.. automodule:: tmtccmd.tm.service_5_event +.. automodule:: tmtccmd.tm.pus_20_fsfw_parameters :members: :undoc-members: :show-inheritance: -tmtccmd.tm.service\_8\_fsfw\_functional\_cmd module ------------------------------------------------------ +tmtccmd.tm.pus\_200\_fsfw\_modes module +-------------------------------------------- -.. automodule:: tmtccmd.tm.service_8_fsfw_functional_cmd +.. automodule:: tmtccmd.tm.pus_200_fsfw_modes :members: :undoc-members: :show-inheritance: -Module contents ---------------- +tmtccmd.tm.base module +---------------------------------- -.. automodule:: tmtccmd.tm +.. automodule:: tmtccmd.tm.base :members: :undoc-members: :show-inheritance: diff --git a/docs/api/tmtccmd.utility.rst b/docs/api/tmtccmd.utility.rst index eaab90b8..71aa1294 100644 --- a/docs/api/tmtccmd.utility.rst +++ b/docs/api/tmtccmd.utility.rst @@ -20,10 +20,10 @@ tmtccmd.utility.hammingcode module :undoc-members: :show-inheritance: -tmtccmd.utility.json\_handler module +tmtccmd.utility.json module ------------------------------------ -.. automodule:: tmtccmd.utility.json_handler +.. automodule:: tmtccmd.utility.json :members: :undoc-members: :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py index 1a90a41f..3947c530 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,8 +13,8 @@ import os import sys -sys.path.insert(0, os.path.abspath("../src")) -import tmtccmd +sys.path.insert(0, os.path.abspath("..")) +from tmtccmd import __version__ # -- Project information ----------------------------------------------------- @@ -23,7 +23,7 @@ author = "Robin Mueller" # The full version, including alpha/beta/rc tags -version = release = tmtccmd.__version__ +version = release = __version__ # -- General configuration --------------------------------------------------- @@ -58,7 +58,7 @@ # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = "logo_tmtccmd.png" +html_logo = "../misc/logo.png" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -69,4 +69,4 @@ # The name of an image file (relative to this directory) to place at the top of # the title page. -latex_logo = "logo_tmtccmd.png" +latex_logo = "../misc/logo.png" diff --git a/docs/gettingstarted.rst b/docs/gettingstarted.rst index 8d4084aa..0ba54938 100644 --- a/docs/gettingstarted.rst +++ b/docs/gettingstarted.rst @@ -2,91 +2,16 @@ Getting Started =============== -The example provided in the ``example`` folder of the Python package is a good place to get started. -You can run the ``tmtccli.py`` file to test the CLI interface or the ``tmtcgui.py`` file -to test the GUI interface. The only working communication interface for the example applications is -the ``dummy`` interface. +Example Project +================ -In general, the main function will only consist of a few calls to the :py:mod:`tmtccmd` package. -The first step is to import some important modules in the :py:mod:`tmtccmd.runner` module. -The example application for the CLI mode looks like this: +The `example application `_ is the best +way to learn how this framework use and to get started. It shows how to set up handles +classes for TC and TM handling and then ties together all components. -:: +The `EIVE `_ and +`SOURCE `_ project implementation of the TMTC commander +provide more complex implementations. - import tmtccmd.runner as runner - from tmtccmd.ccsds.handler import CcsdsTmHandler, ApidHandler - from tmtccmd.config import SetupArgs, default_json_path - from tmtccmd.logging import get_console_logger - - from config.hook_implementation import ExampleHookClass - from config.definitions import APID, pre_send_cb - from config.tm_handler import default_ccsds_packet_handler - - LOGGER = get_console_logger() - - - def main(): - runner.init_printout(True) - hook_obj = ExampleHookClass(json_cfg_path=default_json_path()) - setup_args = SetupArgs(hook_obj=hook_obj, use_gui=True, apid=APID, cli_args=None) - apid_handler = ApidHandler( - cb=default_ccsds_packet_handler, queue_len=50, user_args=None - ) - ccsds_handler = CcsdsTmHandler() - ccsds_handler.add_tm_handler(apid=APID, handler=apid_handler) - runner.setup(setup_args=setup_args) - runner.add_ccsds_handler(ccsds_handler) - tmtc_backend = runner.create_default_tmtc_backend( - setup_args=setup_args, - tm_handler=ccsds_handler, - ) - tmtc_backend.usr_send_wrapper = (pre_send_cb, None) - runner.run(tmtc_backend=tmtc_backend) - - - if __name__ == "__main__": - main() - - -1. The ``ExampleHookClass`` is located inside the - `examples/config `_ folder and contains all - important hook implementations. -#. An argument parser is created and converted to also parse all CLI arguments required - by ``tmtccmd`` -#. A :py:class:`tmtccmd.config.SetupArgs` class is created which contains most of the - configuration required by ``tmtccmd``. The CLI arguments are also passed to this - class -#. An :py:class:`tmtccmd.ccsds.handler.ApidHandler` is created to handle all telemetry - for the application APID. This handler takes a user callback to handle the packets -#. After that, a generic :py:class:`tmtccmd.ccsds.handler.CcsdsTmHandler` is - created and the APID handler is added to it. This allows specifying different handler for - different APIDs -#. Finally, a TMTC backend is created. A backend is required for the :py:func:`tmtccmd.runner.run` - function. -#. A pre-send callback is added to the backend. Each time a telecommand is sent, this callback - will be called - -Most of the TMTC commander configuration is done through the hook object instance and the setup -object. More information about its implementation will be provided in the :ref:`hook-func-label` -chapter. - -CLI -=== - -If ``tmtccli.py`` is run without any command line arguments the commander core will prompt values -like the service or operation code. These values are passed on to the hook functions, which -allows a developers to package different telecommand stacks for different service and op code -combinations. - -GUI -=== - -Simply run the ``tmtcgui.py`` application and connect to the Dummy communication interface. -After that, you can send a ping command and see the generated replies. - -.. _hook-func-label: - -Implementing the hook function -============================== - -Coming Soon +.. + TODO: More explanations for example diff --git a/docs/index.rst b/docs/index.rst index 0f21217a..acd4a014 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,21 +3,16 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to the TMTC Commander Core documentation! -================================================= - -This module is a generic tool for satellite developers to perform TMTC (Telemetry and Telecommand) -handling and testing via different communication interfaces. Currently, only the PUS standard is -implemented as a packet standard. This tool can be used either as a command line tool -or as a GUI tool but the GUI capabilities are still in an alpha state. -The generic parts were decoupled from the former TMTC program -to allow for easier adaption to other missions. - -This client currently supports the following communication interfaces: - -1. TCP/IP with UDP and TCP -2. Serial Communication using fixed frames or a simple ASCII based transport layer -3. QEMU, using a virtual serial interface +TMTC Commander Documentation +============================= + +This commander application was first developed for the +`SOURCE `_ project to test the on-board +software but has evolved into a small Python framework for satellite developers to perform TMTC +(Telemetry and Telecommand) handling and testing via different communication interfaces. +This tool can be used either as a command line tool or as a GUI tool. The GUI features require a +PyQt5 installation. This package also has dedicated support to send and receive ECSS PUS packets +or other generic CCSDS packets. Other pages (online) diff --git a/docs/introduction.rst b/docs/introduction.rst index 5ce288c2..101ddc78 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -5,15 +5,14 @@ Overview ========= -This commander was written for the `SOURCE`_ project as a way to simplify the -software testing. The goal was to make it as easy as possible to send telecommands (TCs) +The goal of this framework is to make it as easy to send telecommands (TCs) to the On-Board Software (OBSW) running on an external On-Board Computer (OBC) and to analyse the telemetry (TMs) coming back. The following graph shows two possible ways to use the TMTC commander .. image:: images/tmtccmd_usage.PNG - :align: center - + :align: center + The first way assumes that the OBSW can be run on a host computer and starts a TPC/IP server internally. The TMTC commander can then be used to send telecommands via the TCP/IP interface. The second way assumes that the OBSW is run on an external microcontroller. @@ -22,28 +21,34 @@ via Ethernet to a microcontroller running a TCP/IP server are possible as well. .. _`SOURCE`: https://www.ksat-stuttgart.de/en/our-missions/source/ -The application is configured by passing an instance of a special hook object to the commander core -using the ``initialize_tmtc_commander`` function and then running the ``run_tmtc_commander`` -function which also allows to specify whether the CLI or the GUI functionality is used. It is -recommended to implement the class ``TmTcHookBase`` for the hook object instantiation -because this class contains all important functions as abstract functions. +.. + TODO: More docs here, general information how components are used Features ========= -- `Packet Utilisation Standard (PUS)`_ TMTC stack to simplify the packaging of PUS telecommand - packets and the analysis and deserialization of raw PUS telemetry -- Common communication interfaces like a serial interface or a TCP/IP interfaces - to send and receive TMTC packets. -- Listener mode to display incoming packets -- Sequential mode which allows inserting telecommands into a queue - and sending them in a sequential way, allowing to analyse the telemetry - generated for each telecommand separately -- Special internal queue commands which allow operations like informative printouts or send delays -- Components to simplify the handling of housekeeping replies (PUS Service 8) or action command - replies (PUS Service 3) -- Components to automatically deserialize telecommand verification replies (PUS Service 1) - or Event replies (PUS Service 5) +- Special support for `Packet Utilisation Standard (PUS)`_ packets and `CCSDS Space Packets`_. + This library uses the `spacepackets`_ library for most packet implementations. +- Support for both CLI and GUI usage +- Flexibility in the way to specify telecommands to send and how to handle incoming telemetry. + This is done by requiring the user to specify callbacks for both TC specification and TM handling. +- One-Queue Mode for simple command sequences and Multi-Queue for more complex command sequences +- Listener mode to only listen to incoming telemetry +- Basic logger components which can be used to store sent Telecommands and incoming Telemetry + in files +- Some components are tailored towards usage with the + `Flight Software Framework (FSFW) `_. -.. _`Packet Utilisation Standard (PUS)`: https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/ +This framework also has a communication interface abstraction which allows to exchange TMTC through +different channels. The framework currently supports the following communication interfaces: + +1. TCP/IP with the :py:class:`tmtccmd.com_if.udp.UdpComIF` and :py:class:`tmtccmd.com_if.tcp.TcpComIF`. +2. Serial Communication with the :py:class:`tmtccmd.com_if.serial.SerialComIF` using fixed frames + or a simple ASCII based transport layer +3. QEMU, using a virtual serial interface +It is also possible to supply custom interfaces. + +.. _`Packet Utilisation Standard (PUS)`: https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/ +.. _`CCSDS Space Packets`: https://public.ccsds.org/Pubs/133x0b2e1.pdf +.. _`spacepackets`: https://github.com/us-irs/py-spacepackets diff --git a/docs/logo_tmtccmd.png b/docs/logo_tmtccmd.png deleted file mode 100644 index 9441f580..00000000 Binary files a/docs/logo_tmtccmd.png and /dev/null differ diff --git a/docs/logo_tmtccmd_smaller.png b/docs/logo_tmtccmd_smaller.png deleted file mode 100644 index 4f841047..00000000 Binary files a/docs/logo_tmtccmd_smaller.png and /dev/null differ diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 00000000..e12b3db7 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1 @@ +/seqcnt.txt diff --git a/examples/config/definitions.py b/examples/config/definitions.py deleted file mode 100644 index a2c891e6..00000000 --- a/examples/config/definitions.py +++ /dev/null @@ -1,17 +0,0 @@ -from spacepackets.ecss import PusTelecommand - -from tmtccmd.com_if.com_interface_base import CommunicationInterface -from tmtccmd.logging import get_console_logger - -APID = 0xEF -LOGGER = get_console_logger() - - -def pre_send_cb( - data: bytes, - com_if: CommunicationInterface, - cmd_info: PusTelecommand, - _user_args: any, -): - LOGGER.info(cmd_info) - com_if.send(data=data) diff --git a/examples/config/hook_implementation.py b/examples/config/hook_implementation.py deleted file mode 100644 index d9bda02d..00000000 --- a/examples/config/hook_implementation.py +++ /dev/null @@ -1,59 +0,0 @@ -import argparse -from typing import Union, Tuple - -from tmtccmd.config.definitions import ServiceOpCodeDictT -from tmtccmd.config.hook import TmTcHookBase, ObjectIdDictT -from tmtccmd.logging import get_console_logger -from tmtccmd.core.backend import TmTcHandler -from tmtccmd.tc.definitions import TcQueueT -from tmtccmd.com_if.com_interface_base import CommunicationInterface - -from .definitions import APID - -LOGGER = get_console_logger() - - -class ExampleHookClass(TmTcHookBase): - def __init__(self, json_cfg_path: str): - super().__init__(json_cfg_path=json_cfg_path) - - def assign_communication_interface( - self, com_if_key: str - ) -> Union[CommunicationInterface, None]: - from tmtccmd.config.com_if import create_communication_interface_default - - LOGGER.info("Communication interface assignment function was called") - return create_communication_interface_default( - com_if_key=com_if_key, - json_cfg_path=self.json_cfg_path, - ) - - def perform_mode_operation(self, tmtc_backend: TmTcHandler, mode: int): - LOGGER.info("Mode operation hook was called") - pass - - def pack_service_queue( - self, service: Union[str, int], op_code: str, service_queue: TcQueueT - ): - from tmtccmd.tc.packer import default_service_queue_preparation - - LOGGER.info("Service queue packer hook was called") - default_service_queue_preparation( - service=service, op_code=op_code, service_queue=service_queue - ) - - def get_object_ids(self) -> ObjectIdDictT: - from tmtccmd.config.objects import get_core_object_ids - - return get_core_object_ids() - - def get_service_op_code_dictionary(self) -> ServiceOpCodeDictT: - from tmtccmd.config.globals import get_default_service_op_code_dict - - return get_default_service_op_code_dict() - - @staticmethod - def handle_service_8_telemetry( - object_id: int, action_id: int, custom_data: bytearray - ) -> Tuple[list, list]: - pass diff --git a/examples/config/tm_handler.py b/examples/config/tm_handler.py deleted file mode 100644 index f6fe5559..00000000 --- a/examples/config/tm_handler.py +++ /dev/null @@ -1,31 +0,0 @@ -from spacepackets.ecss import PusTelemetry -from tmtccmd.tm.pus_17_test import Service17TMExtended -from tmtccmd.tm import Service5Tm -from tmtccmd.utility.tmtc_printer import FsfwTmTcPrinter -from tmtccmd.logging import get_console_logger - -LOGGER = get_console_logger() -FSFW_PRINTER = FsfwTmTcPrinter(None) - - -def default_ccsds_packet_handler(_apid: int, raw_tm_packet: bytes, _user_args: any): - """Default implementation only prints the packet""" - default_factory_hook(raw_tm_packet=raw_tm_packet) - - -def default_factory_hook(raw_tm_packet: bytes): - printer = FsfwTmTcPrinter(None) - service_type = raw_tm_packet[7] - tm_packet = None - if service_type == 1: - tm_packet = Service17TMExtended.unpack(raw_telemetry=raw_tm_packet) - if service_type == 5: - tm_packet = Service5Tm.unpack(raw_telemetry=raw_tm_packet) - if service_type == 17: - tm_packet = Service17TMExtended.unpack(raw_telemetry=raw_tm_packet) - if tm_packet is None: - LOGGER.info( - f"The service {service_type} is not implemented in Telemetry Factory" - ) - tm_packet = PusTelemetry.unpack(raw_telemetry=raw_tm_packet) - printer.handle_long_tm_print(packet_if=tm_packet, info_if=tm_packet) diff --git a/examples/tmtcc.py b/examples/tmtcc.py new file mode 100755 index 00000000..d0beda93 --- /dev/null +++ b/examples/tmtcc.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""Example application for the TMTC Commander""" +import sys +import time +from typing import Optional + +import tmtccmd +from spacepackets.ecss import PusTelemetry, PusTelecommand, PusVerificator +from spacepackets.ecss.pus_1_verification import UnpackParams + +from tmtccmd import CcsdsTmtcBackend, TcHandlerBase +from tmtccmd.pus import VerificationWrapper, FileSeqCountProvider +from tmtccmd.tm import CcsdsTmHandler, SpecificApidHandlerBase +from tmtccmd.com_if import ComInterface +from tmtccmd.config import ( + default_json_path, + SetupParams, + TmTcCfgHookBase, + TmTcDefWrapper, + CoreServiceList, +) +from tmtccmd.config import ArgParserWrapper, SetupWrapper +from tmtccmd.core import BackendController, BackendRequest +from tmtccmd.logging import get_console_logger +from tmtccmd.logging.pus import ( + RegularTmtcLogWrapper, + RawTmtcTimedLogWrapper, + TimedLogWhen, +) +from tmtccmd.tc import ( + QueueEntryHelper, + TcQueueEntryType, + ProcedureHelper, + TcProcedureType, + FeedWrapper, +) +from tmtccmd.tm.pus_5_event import Service5Tm +from tmtccmd.tm.pus_17_test import Service17TmExtended +from tmtccmd.tm.pus_1_verification import Service1TmExtended +from tmtccmd.utility.obj_id import ObjectIdDictT + +from tmtccmd.utility.tmtc_printer import FsfwTmTcPrinter + +LOGGER = get_console_logger() +EXAMPLE_APID = 0xEF + + +class ExampleHookClass(TmTcCfgHookBase): + def __init__(self, json_cfg_path: str): + super().__init__(json_cfg_path=json_cfg_path) + + def assign_communication_interface(self, com_if_key: str) -> Optional[ComInterface]: + from tmtccmd.config.com_if import create_communication_interface_default + + LOGGER.info("Communication interface assignment function was called") + return create_communication_interface_default( + com_if_key=com_if_key, + json_cfg_path=self.json_cfg_path, + ) + + def get_tmtc_definitions(self) -> TmTcDefWrapper: + from tmtccmd.config.globals import get_default_tmtc_defs + + return get_default_tmtc_defs() + + def perform_mode_operation(self, tmtc_backend: CcsdsTmtcBackend, mode: int): + LOGGER.info("Mode operation hook was called") + pass + + def get_object_ids(self) -> ObjectIdDictT: + from tmtccmd.config.objects import get_core_object_ids + + return get_core_object_ids() + + +class PusHandler(SpecificApidHandlerBase): + def __init__( + self, + verif_wrapper: VerificationWrapper, + printer: FsfwTmTcPrinter, + raw_logger: RawTmtcTimedLogWrapper, + ): + super().__init__(EXAMPLE_APID, None) + self.printer = printer + self.raw_logger = raw_logger + self.verif_wrapper = verif_wrapper + + def handle_tm(self, packet: bytes, _user_args: any): + try: + tm_packet = PusTelemetry.unpack(packet) + except ValueError: + LOGGER.warning("Could not generate PUS TM object from raw data") + LOGGER.warning(f"Raw Packet: [{packet.hex(sep=',')}], REPR: {packet!r}") + return + service = tm_packet.service + dedicated_handler = False + if service == 1: + tm_packet = Service1TmExtended.unpack( + data=packet, params=UnpackParams(1, 1) + ) + res = self.verif_wrapper.add_tm(tm_packet) + if res is None: + LOGGER.info( + f"Received Verification TM[{tm_packet.service}, {tm_packet.subservice}] " + f"with Request ID {tm_packet.tc_req_id.as_u32():#08x}" + ) + LOGGER.warning( + f"No matching telecommand found for {tm_packet.tc_req_id}" + ) + else: + self.verif_wrapper.log_to_console(tm_packet, res) + self.verif_wrapper.log_to_file(tm_packet, res) + dedicated_handler = True + if service == 5: + tm_packet = Service5Tm.unpack(packet) + if service == 17: + tm_packet = Service17TmExtended.unpack(packet) + if tm_packet is None: + LOGGER.info( + f"The service {service} is not implemented in Telemetry Factory" + ) + tm_packet = PusTelemetry.unpack(packet) + self.raw_logger.log_tm(tm_packet) + if not dedicated_handler and tm_packet is not None: + self.printer.handle_long_tm_print(packet_if=tm_packet, info_if=tm_packet) + + +class TcHandler(TcHandlerBase): + def __init__( + self, + seq_count_provider: FileSeqCountProvider, + verif_wrapper: VerificationWrapper, + ): + super(TcHandler, self).__init__() + self.seq_count_provider = seq_count_provider + self.verif_wrapper = verif_wrapper + + def send_cb(self, entry_helper: QueueEntryHelper, com_if: ComInterface): + if entry_helper.is_tc: + if entry_helper.entry_type == TcQueueEntryType.PUS_TC: + pus_tc_wrapper = entry_helper.to_pus_tc_entry() + pus_tc_wrapper.pus_tc.seq_count = ( + self.seq_count_provider.next_seq_count() + ) + self.verif_wrapper.add_tc(pus_tc_wrapper.pus_tc) + raw_tc = pus_tc_wrapper.pus_tc.pack() + LOGGER.info(f"Sending {pus_tc_wrapper.pus_tc}") + com_if.send(raw_tc) + + def queue_finished_cb(self, helper: ProcedureHelper): + if helper.proc_type == TcProcedureType.DEFAULT: + def_proc = helper.to_def_procedure() + LOGGER.info( + f"Queue handling finished for service {def_proc.service} and " + f"op code {def_proc.op_code}" + ) + + def feed_cb(self, helper: ProcedureHelper, wrapper: FeedWrapper): + if helper.proc_type == TcProcedureType.DEFAULT: + def_proc = helper.to_def_procedure() + queue_helper = wrapper.queue_helper + service = def_proc.service + if service == CoreServiceList.SERVICE_17.value: + return queue_helper.add_pus_tc(PusTelecommand(service=17, subservice=1)) + + +def main(): + tmtccmd.init_printout(False) + hook_obj = ExampleHookClass(json_cfg_path=default_json_path()) + parser_wrapper = ArgParserWrapper(hook_obj) + parser_wrapper.parse() + params = SetupParams() + parser_wrapper.set_params(params) + params.apid = EXAMPLE_APID + setup_args = SetupWrapper(hook_obj=hook_obj, setup_params=params) + # Create console logger helper and file loggers + tmtc_logger = RegularTmtcLogWrapper() + printer = FsfwTmTcPrinter(tmtc_logger.logger) + raw_logger = RawTmtcTimedLogWrapper(when=TimedLogWhen.PER_HOUR, interval=1) + verificator = PusVerificator() + verification_wrapper = VerificationWrapper(verificator, LOGGER, printer.file_logger) + # Create primary TM handler and add it to the CCSDS Packet Handler + tm_handler = PusHandler(verification_wrapper, printer, raw_logger) + ccsds_handler = CcsdsTmHandler(generic_handler=None) + ccsds_handler.add_apid_handler(tm_handler) + + # Create TC handler + seq_count_provider = FileSeqCountProvider() + tc_handler = TcHandler(seq_count_provider, verification_wrapper) + tmtccmd.setup(setup_args=setup_args) + + tmtc_backend = tmtccmd.create_default_tmtc_backend( + setup_wrapper=setup_args, tm_handler=ccsds_handler, tc_handler=tc_handler + ) + tmtccmd.start(tmtc_backend=tmtc_backend, hook_obj=hook_obj) + ctrl = BackendController() + try: + while True: + state = tmtc_backend.periodic_op(ctrl) + if state.request == BackendRequest.TERMINATION_NO_ERROR: + sys.exit(0) + elif state.request == BackendRequest.DELAY_IDLE: + LOGGER.info("TMTC Client in IDLE mode") + time.sleep(3.0) + elif state.request == BackendRequest.DELAY_LISTENER: + time.sleep(0.8) + elif state.request == BackendRequest.DELAY_CUSTOM: + time.sleep(state.next_delay) + elif state.request == BackendRequest.CALL_NEXT: + pass + except KeyboardInterrupt: + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/examples/tmtccli.py b/examples/tmtccli.py deleted file mode 100755 index e24da7f1..00000000 --- a/examples/tmtccli.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -"""Example application for the TMTC Commander""" -import tmtccmd.runner -from tmtccmd.ccsds.handler import CcsdsTmHandler, ApidHandler -from tmtccmd.config import SetupArgs, default_json_path -from tmtccmd.config.args import ( - create_default_args_parser, - add_default_tmtccmd_args, - parse_default_input_arguments, -) -from tmtccmd.logging import get_console_logger - -from config.hook_implementation import ExampleHookClass -from config.definitions import APID, pre_send_cb -from config.tm_handler import default_ccsds_packet_handler - -LOGGER = get_console_logger() - - -def main(): - tmtccmd.runner.init_printout(False) - hook_obj = ExampleHookClass(json_cfg_path=default_json_path()) - arg_parser = create_default_args_parser() - add_default_tmtccmd_args(arg_parser) - args = parse_default_input_arguments(arg_parser, hook_obj) - setup_args = SetupArgs(hook_obj=hook_obj, use_gui=False, apid=APID, cli_args=args) - apid_handler = ApidHandler( - cb=default_ccsds_packet_handler, queue_len=50, user_args=None - ) - ccsds_handler = CcsdsTmHandler() - ccsds_handler.add_tm_handler(apid=APID, handler=apid_handler) - tmtccmd.runner.setup(setup_args=setup_args) - tmtccmd.runner.add_ccsds_handler(ccsds_handler) - tmtc_backend = tmtccmd.runner.create_default_tmtc_backend( - setup_args=setup_args, - tm_handler=ccsds_handler, - ) - tmtc_backend.usr_send_wrapper = (pre_send_cb, None) - tmtccmd.runner.run(tmtc_backend=tmtc_backend) - - -if __name__ == "__main__": - main() diff --git a/examples/tmtcgui.py b/examples/tmtcgui.py deleted file mode 100755 index 426b9cae..00000000 --- a/examples/tmtcgui.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 -"""Example application for the TMTC Commander""" -import tmtccmd.runner as runner -from tmtccmd.ccsds.handler import CcsdsTmHandler, ApidHandler -from tmtccmd.config import SetupArgs, default_json_path -from tmtccmd.logging import get_console_logger - -from config.hook_implementation import ExampleHookClass -from config.definitions import APID, pre_send_cb -from config.tm_handler import default_ccsds_packet_handler - -LOGGER = get_console_logger() - - -def main(): - runner.init_printout(True) - hook_obj = ExampleHookClass(json_cfg_path=default_json_path()) - setup_args = SetupArgs(hook_obj=hook_obj, use_gui=True, apid=APID, cli_args=None) - apid_handler = ApidHandler( - cb=default_ccsds_packet_handler, queue_len=50, user_args=None - ) - ccsds_handler = CcsdsTmHandler() - ccsds_handler.add_tm_handler(apid=APID, handler=apid_handler) - runner.setup(setup_args=setup_args) - runner.add_ccsds_handler(ccsds_handler) - tmtc_backend = runner.create_default_tmtc_backend( - setup_args=setup_args, - tm_handler=ccsds_handler, - ) - tmtc_backend.usr_send_wrapper = (pre_send_cb, None) - runner.run(tmtc_backend=tmtc_backend) - - -if __name__ == "__main__": - main() diff --git a/misc/logo-arrows-only-small.png b/misc/logo-arrows-only-small.png new file mode 100644 index 00000000..08aee956 Binary files /dev/null and b/misc/logo-arrows-only-small.png differ diff --git a/misc/logo-arrows-only.svg b/misc/logo-arrows-only.svg new file mode 100644 index 00000000..58777351 --- /dev/null +++ b/misc/logo-arrows-only.svg @@ -0,0 +1,588 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/misc/logo-tiny.png b/misc/logo-tiny.png new file mode 100644 index 00000000..d35a6a41 Binary files /dev/null and b/misc/logo-tiny.png differ diff --git a/misc/logo.png b/misc/logo.png new file mode 100644 index 00000000..b809b4b7 Binary files /dev/null and b/misc/logo.png differ diff --git a/misc/logo.svg b/misc/logo.svg new file mode 100644 index 00000000..7d4d6c27 --- /dev/null +++ b/misc/logo.svg @@ -0,0 +1,648 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CMD + TM + TC + + + + + + + + diff --git a/pyproject.toml b/pyproject.toml index b5a3c468..638dd9c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,3 @@ [build-system] -requires = [ - "setuptools>=42", - "wheel" -] -build-backend = "setuptools.build_meta" \ No newline at end of file +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..9c558e35 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +. diff --git a/setup.cfg b/setup.cfg index 32000812..7f4a011c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,20 +32,19 @@ install_requires = colorama>=0.4.4 colorlog>=6.6.0 dle-encoder>=0.2 - spacepackets>=0.8.1 + spacepackets>=0.13.0rc1 prompt-toolkit>=3.0.28 package_dir = - = src + = . packages = find: python_requires = >=3.8 -[options.packages.find] -where = src - [options.extras_require] gui = PyQt5>=5.15.6 - PyQt5-stubs>=5.15.0 + PyQt5-stubs>=5.15.6 +test = + pyfakefs>=4.5.3 [flake8] max-line-length = 100 diff --git a/src/tests/backend_mock.py b/src/tests/backend_mock.py deleted file mode 100644 index a64c9e93..00000000 --- a/src/tests/backend_mock.py +++ /dev/null @@ -1,38 +0,0 @@ -from unittest.mock import MagicMock - -from tmtccmd.core.backend import TmTcHandler -from tmtccmd.ccsds.handler import CcsdsTmHandler -from tmtccmd.config.com_if import create_communication_interface_default -from tmtccmd.sendreceive.tm_listener import TmListener -from tmtccmd.utility.tmtc_printer import FsfwTmTcPrinter, DisplayMode -from tmtccmd.config.definitions import CoreComInterfaces, CoreModeList -from tmtccmd.core.frontend_base import FrontendBase - - -def create_backend_mock(tm_handler: CcsdsTmHandler) -> TmTcHandler: - tmtc_printer = FsfwTmTcPrinter(display_mode=DisplayMode.LONG, file_logger=None) - com_if = create_communication_interface_default( - com_if_key=CoreComInterfaces.DUMMY.value, - json_cfg_path="tmtc_config.json", - ) - tm_listener = TmListener(com_if=com_if, seq_timeout=3.0) - # The global variables are set by the argument parser. - tmtc_backend = TmTcHandler( - com_if=com_if, - tm_listener=tm_listener, - init_mode=CoreModeList.IDLE, - init_service=17, - init_opcode="0", - tm_handler=tm_handler, - ) - tmtc_backend.start_listener = MagicMock(return_value=0) - tmtc_backend.initialize = MagicMock(return_value=0) - return tmtc_backend - - -def create_frontend_mock() -> FrontendBase: - from tmtccmd.core.frontend_base import FrontendBase - - tmtc_frontend = FrontendBase() - tmtc_frontend.start = MagicMock(return_value=0) - return tmtc_frontend diff --git a/src/tests/hook_obj_mock.py b/src/tests/hook_obj_mock.py deleted file mode 100644 index ccb6c27a..00000000 --- a/src/tests/hook_obj_mock.py +++ /dev/null @@ -1,151 +0,0 @@ -from abc import abstractmethod -from typing import Dict, Union, Optional, Tuple -from unittest.mock import MagicMock -import argparse - -from tmtccmd.utility.tmtc_printer import FsfwTmTcPrinter -from tmtccmd.config.com_if import CommunicationInterface -from tmtccmd.config.definitions import DEFAULT_APID -from tmtccmd.config.definitions import ServiceOpCodeDictT, CoreModeList -from tmtccmd.tm.pus_3_hk_base import Service3Base -from tmtccmd.core.backend import TmTcHandler -from tmtccmd.tc.definitions import TcQueueT -from tmtccmd.config.hook import TmTcHookBase -from tmtccmd.logging import get_console_logger - -LOGGER = get_console_logger() - - -def create_hook_mock() -> TmTcHookBase: - """Create simple minimal hook mock using the MagicMock facilities by unittest - :return: - """ - tmtc_hook_base = TmTcHookBase() - tmtc_hook_base.add_globals_pre_args_parsing = MagicMock(return_value=0) - tmtc_hook_base.add_globals_post_args_parsing = MagicMock(return_value=0) - tmtc_hook_base.custom_args_parsing = MagicMock( - return_value=argparse.Namespace(service=17, mode=CoreModeList.IDLE) - ) - return tmtc_hook_base - - -def create_hook_mock_with_srv_handlers() -> TmTcHookBase: - tmtc_hook_base = create_hook_mock() - tmtc_hook_base.handle_service_8_telemetry = MagicMock(return_value=(["Test"], [0])) - # Valid returnvalue for now - srv_3_return_tuple = (["Test"], [0], bytearray(0b10000000), 1) - tmtc_hook_base.handle_service_3_housekeeping = MagicMock( - return_value=srv_3_return_tuple - ) - return tmtc_hook_base - - -class TestHookObj(TmTcHookBase): - service_8_handler_called = False - service_5_handler_called = False - service_3_handler_called = False - - def __init__(self): - super(self, TmTcHookBase).__init__() - self.get_obj_id_called = False - self.add_globals_pre_args_parsing_called = False - self.add_globals_post_args_parsing_called = False - self.assign_communication_interface_called = False - - @abstractmethod - def get_object_ids(self) -> Dict[bytes, list]: - """The user can specify an object ID dictionary here mapping object ID bytearrays to a - list. This list could contain containing the string representation or additional - information about that object ID. - """ - return TmTcHookBase.get_object_ids() - - @abstractmethod - def add_globals_pre_args_parsing(self, gui: bool = False): - """Add all global variables prior to parsing the CLI arguments. - - :param gui: Set to true if the GUI mode is used - :return: - """ - from tmtccmd.config.globals import set_default_globals_pre_args_parsing - - set_default_globals_pre_args_parsing(gui=gui, apid=DEFAULT_APID) - - @abstractmethod - def add_globals_post_args_parsing(self, args: argparse.Namespace): - """Add global variables prior after parsing the CLI arguments. - - :param args: Specify whether a GUI is used - """ - from tmtccmd.config.globals import set_default_globals_post_args_parsing - - set_default_globals_post_args_parsing( - args=args, json_cfg_path=self.get_json_config_file_path() - ) - - @abstractmethod - def assign_communication_interface( - self, com_if_key: str - ) -> Optional[CommunicationInterface]: - """Assign the communication interface used by the TMTC commander to send and receive - TMTC with. - - :param com_if_key: String key of the communication interface to be created. - """ - from tmtccmd.config.com_if import create_communication_interface_default - - return create_communication_interface_default( - com_if_key=com_if_key, - json_cfg_path=self.get_json_config_file_path(), - ) - - @abstractmethod - def get_service_op_code_dictionary(self) -> ServiceOpCodeDictT: - """This is a dicitonary mapping services represented by strings to an operation code - dictionary. - - :return: - """ - from tmtccmd.config.globals import get_default_service_op_code_dict - - return get_default_service_op_code_dict() - - @abstractmethod - def perform_mode_operation(self, tmtc_backend: TmTcHandler, mode: int): - """Perform custom mode operations - :param tmtc_backend: - :param mode: - :return: - """ - pass - - @abstractmethod - def pack_service_queue( - self, service: Union[int, str], op_code: str, service_queue: TcQueueT - ): - """Overriding this function allows the user to package a telecommand queue for a given - service and operation code combination. - - :param service: - :param op_code: - :param service_queue: - :return: - """ - pass - - @staticmethod - def handle_service_8_telemetry( - object_id: bytes, action_id: int, custom_data: bytearray - ) -> Tuple[list, list]: - """This function is called by the TMTC core to handle Service 8 packets - The user can return a tuple of two lists, where the first list - is a list of header strings to print and the second list is a list of values to print. - The TMTC core will take care of printing both lists and logging them. - - :param object_id: Byte representation of the object ID - :param action_id: - :param custom_data: - :return: - """ - TestHookObj.service_8_handler_called = True - return [], [] diff --git a/src/tests/test_obj_id_manager.py b/src/tests/test_obj_id_manager.py deleted file mode 100644 index db1facbf..00000000 --- a/src/tests/test_obj_id_manager.py +++ /dev/null @@ -1,11 +0,0 @@ -from unittest import TestCase -from tmtccmd.core.object_id_manager import insert_object_id, get_object_id_info - -TEST_ID_0 = bytes([0x00, 0x01, 0x02, 0x03]) - - -class TestObjIdManager(TestCase): - def test_obj_id_manager(self): - insert_object_id(object_id=TEST_ID_0, object_id_info=["TEST_ID_0"]) - info_list = get_object_id_info(object_id=TEST_ID_0) - self.assertTrue(info_list[0] == "TEST_ID_0") diff --git a/src/tests/test_printer.py b/src/tests/test_printer.py deleted file mode 100644 index a1b9a160..00000000 --- a/src/tests/test_printer.py +++ /dev/null @@ -1,51 +0,0 @@ -import shutil -import os -from unittest import TestCase - -from spacepackets.ccsds.time import CdsShortTimestamp - -from tmtccmd.tm.pus_1_verification import Service1TMExtended -from tmtccmd.pus.pus_17_test import pack_service_17_ping_command -from tmtccmd.utility.tmtc_printer import FsfwTmTcPrinter -from tmtccmd.logging import get_console_logger, LOG_DIR -from tmtccmd.config.globals import update_global, CoreGlobalIds -from tmtccmd.logging.pus import ( - log_raw_pus_tc, - log_raw_pus_tm, - get_current_raw_file_name, - create_tmtc_logger, -) - - -class TestPrintersLoggers(TestCase): - def setUp(self): - if os.path.exists(LOG_DIR): - shutil.rmtree(LOG_DIR) - os.mkdir(LOG_DIR) - self.tmtc_printer = FsfwTmTcPrinter(file_logger=create_tmtc_logger()) - self.logger = get_console_logger() - - def test_pus_loggers(self): - pus_tc = pack_service_17_ping_command(ssc=0) - file_name = get_current_raw_file_name() - log_raw_pus_tc(pus_tc.pack()) - pus_tm = Service1TMExtended( - subservice=1, time=CdsShortTimestamp.init_from_current_time() - ) - log_raw_pus_tm(pus_tm.pack()) - log_raw_pus_tc( - pus_tc.pack(), srv_subservice=(pus_tc.service, pus_tc.subservice) - ) - log_raw_pus_tm( - pus_tm.pack(), srv_subservice=(pus_tm.service, pus_tm.subservice) - ) - self.assertTrue(os.path.exists(file_name)) - - def test_print_functions(self): - pass - - def tearDown(self) -> None: - """Reset the hook object""" - update_global(CoreGlobalIds.TMTC_HOOK, None) - if os.path.exists(LOG_DIR): - shutil.rmtree(LOG_DIR) diff --git a/src/tests/test_runner.py b/src/tests/test_runner.py deleted file mode 100644 index 89005a60..00000000 --- a/src/tests/test_runner.py +++ /dev/null @@ -1,47 +0,0 @@ -from unittest import TestCase -from tmtccmd.ccsds.handler import CcsdsTmHandler -import tmtccmd.runner as tmtccmd - -from tests.backend_mock import create_backend_mock, create_frontend_mock -from tests.hook_obj_mock import create_hook_mock - - -class TestTmtcRunner(TestCase): - def test_tmtc_runner(self): - # TODO: Update tests for updated API - """ - hook_base = create_hook_mock() - tm_handler = CcsdsTmHandler() - init_tmtccmd(hook_object=hook_base) - setup_tmtccmd(use_gui=False, reduced_printout=False) - backend_mock = create_backend_mock(tm_handler=tm_handler) - run_tmtccmd(tmtc_backend=backend_mock) - backend_mock.start_listener.assert_called_with() - backend_mock.initialize.assert_called_with() - """ - - # TODO: Maybe we can remove this test altogether.. - """ - frontend_mock = create_frontend_mock() - run_tmtccmd( - use_gui=True, - tmtc_backend=backend_mock, - tmtc_frontend=frontend_mock, - ) - frontend_mock.start.assert_called_once() - qt_app = frontend_mock.start.call_args[0][0] - # TODO: Fix test - # self.assertTrue(qt_app is None) - - default_backend = get_default_tmtc_backend( - hook_obj=hook_base, tm_handler=tm_handler, json_cfg_path="tmtc_config.json" - ) - self.assertTrue(default_backend is not None) - """ - - def test_errors(self): - # TODO: API has changed, update tests - # self.assertRaises(ValueError, init_tmtccmd, None) - # self.assertRaises(TypeError, run_tmtccmd) - # self.assertRaises(RuntimeError, run_tmtccmd, False) - pass diff --git a/src/tests/test_sendreceive.py b/src/tests/test_sendreceive.py deleted file mode 100644 index 9093e9b2..00000000 --- a/src/tests/test_sendreceive.py +++ /dev/null @@ -1,5 +0,0 @@ -from unittest import TestCase - - -class TestSendReceive(TestCase): - pass diff --git a/src/tmtccmd/__init__.py b/src/tmtccmd/__init__.py deleted file mode 100644 index 2adbf251..00000000 --- a/src/tmtccmd/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -VERSION_NAME = "tmtccmd" -VERSION_MAJOR = 2 -VERSION_MINOR = 2 -VERSION_REVISION = 2 - -# I think this needs to be in string representation to be parsed so we can't -# use a formatted string here. -__version__ = "2.2.2" diff --git a/src/tmtccmd/ccsds/__init__.py b/src/tmtccmd/ccsds/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tmtccmd/ccsds/handler.py b/src/tmtccmd/ccsds/handler.py deleted file mode 100644 index c26982e3..00000000 --- a/src/tmtccmd/ccsds/handler.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import Callable, Dict, Optional, Tuple, List, Type, Any - -from tmtccmd.tm.handler import TmHandler -from tmtccmd.tm.definitions import TelemetryQueueT, TmTypes -from tmtccmd.sendreceive.tm_listener import QueueListT -from tmtccmd.utility.tmtc_printer import FsfwTmTcPrinter -from tmtccmd.logging import get_console_logger - -LOGGER = get_console_logger() - -CcsdsCallbackT = Callable[[int, bytes, Any], None] - - -class ApidHandler: - def __init__(self, cb: CcsdsCallbackT, queue_len: int, user_args: any): - self.callback: CcsdsCallbackT = cb - self.queue_len: int = queue_len - self.user_args: any = user_args - - -HandlerDictT = Dict[int, ApidHandler] - - -class CcsdsTmHandler(TmHandler): - """Generic CCSDS handler class. The user can create an instance of this class to handle - CCSDS packets with different APIDs""" - - def __init__(self): - super().__init__(tm_type=TmTypes.CCSDS_SPACE_PACKETS) - self._handler_dict: HandlerDictT = dict() - - def add_tm_handler(self, apid: int, handler: ApidHandler): - """Add a TM handler for a certain APID. The handler is a callback function which - will be called if telemetry with that APID arrives - :param apid: CCSDS Application Process ID - :param handler: Handler class instance - :return: - """ - self._handler_dict[apid] = handler - - def get_apid_queue_len_list(self) -> List[Tuple[int, int]]: - list = [] - for apid, handler_value in self._handler_dict.items(): - list.append((apid, handler_value.queue_len)) - return list - - def handle_packet_queues(self, packet_queue_list: QueueListT): - for queue_tuple in packet_queue_list: - apid = queue_tuple[0] - handler_obj = self._handler_dict.get(apid) - if handler_obj is not None: - self.handle_ccsds_packet_queue( - tm_queue=queue_tuple[1], apid=apid, handler=handler_obj - ) - - def handle_ccsds_packet_queue( - self, - tm_queue: TelemetryQueueT, - apid: int, - handler: Optional[ApidHandler] = None, - ): - if handler is None: - handler = self._handler_dict.get(apid) - for tm_packet in tm_queue: - if handler is not None: - handler.callback(apid, tm_packet, handler.user_args) - else: - LOGGER.warning(f"No valid handler for TM with APID {apid} found") diff --git a/src/tmtccmd/cfdp/__init__.py b/src/tmtccmd/cfdp/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tmtccmd/com_if/__init__.py b/src/tmtccmd/com_if/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tmtccmd/com_if/dummy_com_if.py b/src/tmtccmd/com_if/dummy_com_if.py deleted file mode 100644 index 11560248..00000000 --- a/src/tmtccmd/com_if/dummy_com_if.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Dummy Communication Interface. Currently serves to provide an example without external hardware -""" -from spacepackets.ecss.tc import PusTelecommand -from spacepackets.ccsds.spacepacket import ( - get_space_packet_sequence_control, - SequenceFlags, -) - -from tmtccmd.com_if.com_interface_base import CommunicationInterface -from tmtccmd.tm import TelemetryListT -from tmtccmd.tm.pus_1_verification import Service1TMExtended -from tmtccmd.tm.pus_17_test import Subservices, Service17TMExtended -from tmtccmd.logging import get_console_logger -from tmtccmd.utility.tmtc_printer import FsfwTmTcPrinter - -LOGGER = get_console_logger() - - -class DummyComIF(CommunicationInterface): - def __init__(self, com_if_key: str): - super().__init__(com_if_key=com_if_key) - self.dummy_handler = DummyHandler() - self.last_service = 0 - self.last_subservice = 0 - self.tc_ssc = 0 - self.tc_packet_id = 0 - - def initialize(self, args: any = None) -> any: - pass - - def open(self, args: any = None) -> None: - pass - - def close(self, args: any = None) -> None: - pass - - def data_available(self, timeout: float = 0, parameters: any = 0): - if self.dummy_handler.reply_pending: - return True - return False - - def receive(self, parameters: any = 0) -> TelemetryListT: - return self.dummy_handler.receive_reply_package() - - def send(self, data: bytearray): - if data is not None: - self.dummy_handler.pass_telecommand(data=data) - - -class DummyHandler: - def __init__(self): - self.last_telecommand = None - self.next_telemetry_package = [] - self.last_tc_ssc = 0 - self.last_tc_packet_id = 0 - self.current_ssc = 0 - self.reply_pending = False - - def pass_telecommand(self, data: bytearray): - # TODO: Need TC deserializer for cleaner implementation - self.last_telecommand = data - self.last_tc_ssc = ((data[2] << 8) | data[3]) & 0x3FFF - self.last_service = data[7] - self.last_subservice = data[8] - self.tc_packet_id = data[0] << 8 | data[1] - self.reply_pending = True - self.generate_reply_package() - - def generate_reply_package(self): - """ - Generate the replies. This function will perform the following steps: - - Generate an object representation of the telemetry to be generated based on service and subservice - - Generate the raw bytearray of the telemetry - - Generate the object representation which would otherwise be generated from the raw bytearray received - from an external source - """ - if self.last_service == 17: - if self.last_subservice == 1: - tc_psc = get_space_packet_sequence_control( - sequence_flags=SequenceFlags.UNSEGMENTED, - source_sequence_count=self.last_tc_ssc, - ) - tm_packer = Service1TMExtended( - subservice=1, - ssc=self.current_ssc, - tc_packet_id=self.last_tc_packet_id, - tc_psc=tc_psc, - ) - - self.current_ssc += 1 - tm_packet_raw = tm_packer.pack() - self.next_telemetry_package.append(tm_packet_raw) - tm_packer = Service1TMExtended( - subservice=7, - ssc=self.current_ssc, - tc_packet_id=self.last_tc_packet_id, - tc_psc=tc_psc, - ) - tm_packet_raw = tm_packer.pack() - self.next_telemetry_package.append(tm_packet_raw) - self.current_ssc += 1 - - tm_packer = Service17TMExtended(subservice=Subservices.TM_REPLY) - tm_packet_raw = tm_packer.pack() - self.next_telemetry_package.append(tm_packet_raw) - self.current_ssc += 1 - - def receive_reply_package(self) -> TelemetryListT: - if self.reply_pending: - return_list = self.next_telemetry_package.copy() - self.next_telemetry_package.clear() - self.reply_pending = False - return return_list - else: - return [] diff --git a/src/tmtccmd/config/__init__.py b/src/tmtccmd/config/__init__.py deleted file mode 100644 index 56e7e84b..00000000 --- a/src/tmtccmd/config/__init__.py +++ /dev/null @@ -1,115 +0,0 @@ -import argparse -import collections.abc -from typing import Optional, Union, List, Dict - -from tmtccmd.config.globals import ( - CoreServiceList, - add_op_code_entry, - add_service_op_code_entry, - generate_op_code_options, - handle_mode_arg, - check_and_set_other_args, - handle_com_if_arg, -) -from .definitions import ( - QueueCommands, - CoreGlobalIds, - ServiceOpCodeDictT, - OpCodeDictKeys, - HkReplyUnpacked, - DataReplyUnpacked, - default_json_path, -) - -from tmtccmd.logging import get_console_logger -from tmtccmd.core.globals_manager import update_global - -from .hook import TmTcHookBase - - -class SetupArgs: - def __init__( - self, - hook_obj: TmTcHookBase, - use_gui: bool, - apid: int, - cli_args: Optional[argparse.Namespace], - json_cfg_path: Optional[str] = None, - reduced_printout: bool = False, - use_ansi_colors: bool = True, - ): - """This class encapsulates all required objects for the TMTC commander - :param hook_obj: User hook object. Needs to be implemented by the user - :param cli_args: Command line arguments as returned by the ArgumentParser.parse_args method - :param use_gui: Specify whether a GUI is used - :param reduced_printout: - :param use_ansi_colors: - """ - self.hook_obj = hook_obj - self.use_gui = use_gui - self.json_cfg_path = json_cfg_path - self.reduced_printout = reduced_printout - self.ansi_colors = use_ansi_colors - self.cli_args = cli_args - self.json_cfg_path = json_cfg_path - self.tc_apid = apid - self.tm_apid = apid - if json_cfg_path is None: - self.json_cfg_path = default_json_path() - - -def pass_cli_args( - setup_args: SetupArgs, - custom_modes_list: Optional[List[Union[collections.abc.Iterable, Dict]]] = None, - custom_services_list: Optional[List[Union[collections.abc.Iterable, Dict]]] = None, - custom_com_if_dict: Dict[str, any] = None, -): - """This function takes the argument namespace as a parameter and determines - a set of globals from the parsed arguments. - If custom dictionaries are specified, the developer should take care of specifying - integers as keys and the string representation of the command line argument as value. - This will be used for internalization. - - :param setup_args: Setup arguments - :param custom_modes_list: List of collections or dictionaries containing custom modes - :param custom_services_list: List of collections or dictionaries containing custom services - :param custom_com_if_dict: List of collections or dictionaries containing customcommunication interfaces - :return: - """ - logger = get_console_logger() - args = setup_args.cli_args - handle_mode_arg(args=args, custom_modes_list=custom_modes_list) - handle_com_if_arg( - args=args, - json_cfg_path=setup_args.json_cfg_path, - custom_com_if_dict=custom_com_if_dict, - ) - - display_mode_param = "long" - if args.sh_display is not None: - if args.sh_display: - display_mode_param = "short" - else: - display_mode_param = "long" - update_global(CoreGlobalIds.DISPLAY_MODE, display_mode_param) - - try: - service_param = args.service - except AttributeError: - logger.warning( - "Passed namespace does not contain the service (-s) argument. " - "Setting test service ID (17)" - ) - service_param = CoreServiceList.SERVICE_17.value - update_global(CoreGlobalIds.CURRENT_SERVICE, service_param) - - if args.op_code is None: - op_code = 0 - else: - op_code = str(args.op_code).lower() - update_global(CoreGlobalIds.OP_CODE, op_code) - - try: - check_and_set_other_args(args=args) - except AttributeError: - logger.exception("Passed arguments are missing components.") diff --git a/src/tmtccmd/config/args.py b/src/tmtccmd/config/args.py deleted file mode 100644 index f030c9f6..00000000 --- a/src/tmtccmd/config/args.py +++ /dev/null @@ -1,412 +0,0 @@ -""" -Argument parser modules for the TMTC commander core -""" -import argparse -import sys -from typing import Optional - -from prompt_toolkit.completion import WordCompleter -from prompt_toolkit.shortcuts import CompleteStyle -import prompt_toolkit - -from tmtccmd.config.definitions import ( - CoreModeList, - ServiceOpCodeDictT, - OpCodeEntryT, - OpCodeDictKeys, -) -from tmtccmd.config.hook import TmTcHookBase -from tmtccmd.utility.conf_util import AnsiColors -from tmtccmd.logging import get_console_logger - - -LOGGER = get_console_logger() - - -def get_default_descript_txt() -> str: - return ( - f"{AnsiColors.GREEN}TMTC Client Command Line Interface\n" - f"{AnsiColors.RESET}This application provides generic components to execute " - f"TMTC commanding.\n" - f"The developer is expected to specify the packaged telecommands for a given\n" - "service and operation code combination. The developer is also expected\n" - "to implement the handling of telemetry. All these tasks can be done by implementing\n" - "a hook object and passing it to the core." - ) - - -def create_default_args_parser( - descript_txt: Optional[str] = None, -) -> argparse.ArgumentParser: - if descript_txt is None: - descript_txt = get_default_descript_txt() - return argparse.ArgumentParser( - description=descript_txt, formatter_class=argparse.RawTextHelpFormatter - ) - - -def add_default_tmtccmd_args(parser: argparse.ArgumentParser): - add_default_mode_arguments(parser) - add_default_com_if_arguments(parser) - add_generic_arguments(parser) - - add_ethernet_arguments(parser) - - parser.add_argument( - "--tctf", - type=float, - help="TC Timeout Factor. Multiplied with " - "TM Timeout, TC sent again after this time period. Default: 3.5", - default=3.5, - ) - parser.add_argument( - "-r", - "--raw-print", - help="Supply -r to print all raw TM data directly", - action="store_true", - ) - parser.add_argument( - "-d", - "--sh-display", - help="Supply -d to print short output", - action="store_true", - ) - parser.add_argument( - "-k", - "--hk", - dest="print_hk", - help="Supply -k or --hk to print HK data", - action="store_true", - ) - parser.add_argument( - "--rs", - dest="resend_tc", - help="Specify whether TCs are sent again after timeout", - action="store_true", - ) - - -def parse_default_input_arguments( - parser: argparse.ArgumentParser, - hook_obj: TmTcHookBase, - print_known_args: bool = False, - print_unknown_args: bool = False, -) -> argparse.Namespace: - """Parses all input arguments - :return: Input arguments contained in a special namespace and accessable by args. - """ - - if len(sys.argv) == 1: - LOGGER.info( - "No input arguments specified. Run with -h to get list of arguments" - ) - - args, unknown = parser.parse_known_args() - - if print_known_args: - LOGGER.info("Printing known arguments:") - for argument in vars(args): - LOGGER.debug(argument + ": " + str(getattr(args, argument))) - if print_unknown_args: - LOGGER.info("Printing unknown arguments:") - for argument in unknown: - LOGGER.info(argument) - - args_post_processing(args, unknown, hook_obj.get_service_op_code_dictionary()) - return args - - -def add_generic_arguments(arg_parser: argparse.ArgumentParser): - arg_parser.add_argument( - "-s", "--service", type=str, help="Service to test", default=None - ) - arg_parser.add_argument( - "-o", - "--op_code", - help="Operation code, which is passed to the TC packer functions", - default=None, - ) - arg_parser.add_argument( - "-l", - "--listener", - help="Determine whether the listener mode will be active after performing the operation", - action="store_true", - default=None, - ) - arg_parser.add_argument( - "-t", - "--tm_timeout", - type=float, - help="TM Timeout when listening to verification sequence." - " Default: 5 seconds", - default=None, - ) - arg_parser.add_argument( - "--nl", - dest="print_log", - help="Supply --nl to suppress print output to log files.", - action="store_false", - ) - arg_parser.add_argument( - "--np", - dest="print_tm", - help="Supply --np to suppress print output to console.", - action="store_false", - ) - - -def add_default_mode_arguments(arg_parser: argparse.ArgumentParser): - from tmtccmd.config.definitions import CoreModeList, CoreModeStrings - - help_text = "Core Modes. Default: seqcmd\n" - seq_help = ( - f"{CoreModeList.SEQUENTIAL_CMD_MODE} or " - f"{CoreModeStrings[CoreModeList.SEQUENTIAL_CMD_MODE]}: " - f"Sequential Command Mode\n" - ) - listener_help = ( - f"{CoreModeList.LISTENER_MODE} or {CoreModeStrings[CoreModeList.LISTENER_MODE]}: " - f"Listener Mode\n" - ) - gui_help = ( - f"{CoreModeList.GUI_MODE} or " - f"{CoreModeStrings[CoreModeList.GUI_MODE]}: " - f"GUI mode\n" - ) - help_text += seq_help + listener_help + gui_help - arg_parser.add_argument( - "-m", - "--mode", - type=str, - help=help_text, - default=CoreModeStrings[CoreModeList.SEQUENTIAL_CMD_MODE], - ) - - -def add_default_com_if_arguments(arg_parser: argparse.ArgumentParser): - from tmtccmd.config.definitions import CoreComInterfacesDict, CoreComInterfaces - - help_text = ( - "Core Communication Interface. If this is not specified, the commander core\n" - "will try to extract it from the JSON or prompt it from the user.\n" - ) - dummy_line = ( - f"{CoreComInterfacesDict[CoreComInterfaces.DUMMY.value]}: Dummy Interface\n" - ) - udp_line = ( - f"{CoreComInterfacesDict[CoreComInterfaces.TCPIP_UDP.value]}: " f"UDP client\n" - ) - ser_dle_line = ( - f"{CoreComInterfacesDict[CoreComInterfaces.SERIAL_DLE.value]}: " - f"Serial with DLE transport layer\n" - ) - ser_fixed_line = ( - f"{CoreComInterfacesDict[CoreComInterfaces.SERIAL_FIXED_FRAME.value]}: " - f"Serial with fixed frames\n" - ) - ser_qemu_line = ( - f"{CoreComInterfacesDict[CoreComInterfaces.SERIAL_QEMU.value]}: " - f"QEMU serial interface\n" - ) - help_text += dummy_line + ser_dle_line + udp_line + ser_fixed_line + ser_qemu_line - arg_parser.add_argument( - "-c", - "--com_if", - type=str, - help=help_text, - default=CoreComInterfaces.UNSPECIFIED.value, - ) - - -def add_ethernet_arguments(arg_parser: argparse.ArgumentParser): - arg_parser.add_argument( - "--client-ip", help="Client(Computer) IP. Default:''", default="" - ) - arg_parser.add_argument( - "--board-ip", help="Board IP. Default: Localhost 127.0.0.1", default="127.0.0.1" - ) - - -def args_post_processing( - args, unknown: list, service_op_code_dict: ServiceOpCodeDictT -) -> None: - """Handles the parsed arguments. - :param args: Namespace objects - (see https://docs.python.org/dev/library/argparse.html#argparse.Namespace) - :param unknown: List of unknown parameters. - :return: None - """ - if len(unknown) > 0: - print("Unknown arguments detected: " + str(unknown)) - if len(sys.argv) > 1: - handle_unspecified_args(args, service_op_code_dict) - if len(sys.argv) == 1: - handle_empty_args(args, service_op_code_dict) - - -def handle_unspecified_args(args, service_op_code_dict: ServiceOpCodeDictT) -> None: - """If some arguments are unspecified, they are set here with (variable) default values. - :param args: Arguments from calling parse method - :param service_op_code_dict: - :return: None - """ - from tmtccmd.config.definitions import CoreModeStrings - - if args.mode is None: - args.mode = CoreModeStrings[CoreModeList.SEQUENTIAL_CMD_MODE] - if service_op_code_dict is None: - LOGGER.warning("Invalid Service to Op-Code dictionary detected") - if args.service is None: - args.service = "0" - if args.op_code is None: - args.op_code = "0" - return - if args.service is None: - if args.mode == CoreModeStrings[CoreModeList.SEQUENTIAL_CMD_MODE]: - LOGGER.info("No service argument (-s) specified, prompting from user..") - # Try to get the service list from the hook base and prompt service from user - args.service = prompt_service(service_op_code_dict) - if args.op_code is None: - current_service = args.service - args.op_code = prompt_op_code( - service_op_code_dict=service_op_code_dict, service=current_service - ) - if args.service is not None: - service_entry = service_op_code_dict.get(args.service) - if service_entry is not None: - op_code_value = service_op_code_dict[args.service][1] - op_code_options = None - op_code_value = op_code_value.get(args.op_code) - if op_code_value is not None: - op_code_options = op_code_value[1] - if op_code_options is not None and isinstance(op_code_options, dict): - if op_code_options.get(OpCodeDictKeys.ENTER_LISTENER_MODE): - if args.listener is None: - LOGGER.info( - "Detected op code configuration: Enter listener mode after command" - ) - args.listener = True - else: - LOGGER.warning( - "Detected op code listerner mode configuration but is " - "overriden by CLI argument" - ) - timeout = op_code_options.get(OpCodeDictKeys.TIMEOUT) - if timeout is not None: - if args.tm_timeout is None: - LOGGER.info( - f"Detected op code configuration: Set custom timeout {timeout}" - ) - args.tm_timeout = timeout - else: - LOGGER.warning( - "Detected op code timeout configuration but is overriden by " - "CLI argument" - ) - if args.tm_timeout is None: - args.tm_timeout = 5.0 - if args.listener is None: - args.listener = False - - -def handle_empty_args(args, service_op_code_dict: ServiceOpCodeDictT) -> None: - """If no args were supplied, request input from user directly. - :param args: - :return: - """ - LOGGER.info("No arguments specified..") - handle_unspecified_args(args, service_op_code_dict) - - -def prompt_service(service_op_code_dict: ServiceOpCodeDictT) -> str: - service_adjustment = 20 - info_adjustment = 30 - horiz_line_num = service_adjustment + info_adjustment + 3 - horiz_line = horiz_line_num * "-" - service_string = "Service".ljust(service_adjustment) - info_string = "Information".ljust(info_adjustment) - while True: - print(f" {horiz_line}") - print(f"|{service_string} | {info_string}|") - print(f" {horiz_line}") - srv_completer = build_service_word_completer( - service_op_code_dict=service_op_code_dict - ) - for service_entry in service_op_code_dict.items(): - try: - adjusted_service_entry = service_entry[0].ljust(service_adjustment) - adjusted_service_info = service_entry[1][0].ljust(info_adjustment) - print(f"|{adjusted_service_entry} | {adjusted_service_info}|") - except AttributeError: - LOGGER.warning( - f"Error handling service entry {service_entry[0]}. Skipping.." - ) - print(f" {horiz_line}") - service_string = prompt_toolkit.prompt( - "Please select a service by specifying the key: ", - completer=srv_completer, - complete_style=CompleteStyle.MULTI_COLUMN, - ) - if service_string in service_op_code_dict: - LOGGER.info(f"Selected service: {service_string}") - return service_string - else: - LOGGER.warning("Invalid key, try again") - - -def build_service_word_completer( - service_op_code_dict: ServiceOpCodeDictT, -) -> WordCompleter: - srv_list = [] - for service_entry in service_op_code_dict.items(): - srv_list.append(service_entry[0]) - srv_completer = WordCompleter(words=srv_list, ignore_case=True) - return srv_completer - - -def prompt_op_code(service_op_code_dict: ServiceOpCodeDictT, service: str) -> str: - op_code_adjustment = 24 - info_adjustment = 56 - horz_line_num = op_code_adjustment + info_adjustment + 3 - horiz_line = horz_line_num * "-" - op_code_info_str = "Operation Code".ljust(op_code_adjustment) - info_string = "Information".ljust(info_adjustment) - while True: - print(f" {horiz_line}") - print(f"|{op_code_info_str} | {info_string}|") - print(f" {horiz_line}") - if service in service_op_code_dict: - op_code_dict = service_op_code_dict[service][1] - completer = build_op_code_word_completer( - service=service, op_code_dict=op_code_dict - ) - for op_code_entry in op_code_dict.items(): - adjusted_op_code_entry = op_code_entry[0].ljust(op_code_adjustment) - adjusted_op_code_info = op_code_entry[1][0].ljust(info_adjustment) - print(f"|{adjusted_op_code_entry} | {adjusted_op_code_info}|") - print(f" {horiz_line}") - op_code_string = prompt_toolkit.prompt( - "Please select an operation code by specifying the key: ", - completer=completer, - complete_style=CompleteStyle.MULTI_COLUMN, - ) - if op_code_string in op_code_dict: - LOGGER.info(f"Selected op code: {op_code_string}") - return op_code_string - else: - LOGGER.warning("Invalid key, try again") - else: - LOGGER.warning( - "Service not in dictionary. Setting default operation code 0" - ) - return "0" - - -def build_op_code_word_completer( - service: str, op_code_dict: OpCodeEntryT -) -> WordCompleter: - op_code_list = [] - for op_code_entry in op_code_dict.items(): - op_code_list.append(op_code_entry[0]) - op_code_completer = WordCompleter(words=op_code_list, ignore_case=True) - return op_code_completer diff --git a/src/tmtccmd/config/definitions.py b/src/tmtccmd/config/definitions.py deleted file mode 100644 index a101fa08..00000000 --- a/src/tmtccmd/config/definitions.py +++ /dev/null @@ -1,181 +0,0 @@ -"""Definitions for the TMTC commander core -""" -import enum -from typing import Tuple, Dict, Optional, List, Union, Callable, Any - -from spacepackets.ecss import PusTelecommand - -from tmtccmd.com_if.com_interface_base import CommunicationInterface - - -def default_json_path() -> str: - return "tmtc_conf.json" - - -class CoreGlobalIds(enum.IntEnum): - """ - Numbers from 128 to 200 are reserved for core globals - """ - - # Object handles - TMTC_HOOK = 128 - COM_INTERFACE_HANDLE = 129 - TM_LISTENER_HANDLE = 130 - TMTC_PRINTER_HANDLE = 131 - TM_HANDLER_HANDLE = 132 - PRETTY_PRINTER = 133 - - # Parameters - JSON_CFG_PATH = 139 - MODE = 141 - CURRENT_SERVICE = 142 - COM_IF = 144 - OP_CODE = 145 - TM_TIMEOUT = 146 - SERVICE_OP_CODE_DICT = 147 - COM_IF_DICT = 148 - - # Miscellaneous - DISPLAY_MODE = 150 - USE_LISTENER_AFTER_OP = 151 - PRINT_HK = 152 - PRINT_TM = 153 - PRINT_RAW_TM = 154 - PRINT_TO_FILE = 155 - RESEND_TC = 156 - TC_SEND_TIMEOUT_FACTOR = 157 - - # Config dictionaries - USE_SERIAL = 160 - SERIAL_CONFIG = 161 - USE_ETHERNET = 162 - ETHERNET_CONFIG = 163 - END = 300 - - -class OpCodeDictKeys(enum.IntEnum): - TIMEOUT = CoreGlobalIds.TM_TIMEOUT - ENTER_LISTENER_MODE = CoreGlobalIds.USE_LISTENER_AFTER_OP - - -# Service Op Code Dictionary Types -ServiceNameT = str -ServiceInfoT = str -OpCodeNameT = Union[str, List[str]] -OpCodeInfoT = str -# Operation code options are optional. If none are supplied, default values are assumed -OpCodeOptionsT = Optional[Dict[OpCodeDictKeys, any]] -OpCodeEntryT = Dict[OpCodeNameT, Tuple[OpCodeInfoT, OpCodeOptionsT]] -# It is possible to specify a service without any op codes -ServiceDictValueT = Optional[Tuple[ServiceInfoT, OpCodeEntryT]] -ServiceOpCodeDictT = Dict[ServiceNameT, ServiceDictValueT] - -# Com Interface Types -ComIFValueT = Tuple[str, any] -ComIFDictT = Dict[str, ComIFValueT] - -EthernetAddressT = Tuple[str, int] - - -class QueueCommands(enum.Enum): - PRINT = "print" - RAW_PRINT = "raw_print" - WAIT = "wait" - SET_TIMEOUT = "set_timeout" - - -TcQueueEntryArg = Any -UserArg = Any -"""Third Argument: Second argument in TC queue tuple. Fouth Argument -""" -UsrSendCbT = Callable[ - [Union[bytes, QueueCommands], CommunicationInterface, TcQueueEntryArg, UserArg], - None, -] - - -class DataReplyUnpacked: - def __init__(self): - # Name of the data fields inside a data set - self.header_list = [] - # Corresponding list of content - self.content_list = [] - - -class HkReplyUnpacked(DataReplyUnpacked): - def __init__(self): - super().__init__() - # Validity buffer - self.validity_buffer = bytearray() - # Number of variables contained in HK set - self._num_of_vars = None - - @property - def num_of_vars(self): - """Unless set to a specific number, will return the length of the content list - :return: - """ - if self._num_of_vars is None: - return len(self.header_list) - return self._num_of_vars - - @num_of_vars.setter - def num_of_vars(self, num_of_vars: int): - self._num_of_vars = num_of_vars - - -class CoreComInterfaces(enum.Enum): - DUMMY = "dummy" - SERIAL_DLE = "ser_dle" - TCPIP_UDP = "udp" - TCPIP_TCP = "tcp" - SERIAL_FIXED_FRAME = "ser_fixed" - SERIAL_QEMU = "ser_qemu" - UNSPECIFIED = "unspec" - - -CoreComInterfacesDict = { - CoreComInterfaces.DUMMY.value: "Dummy Interface", - CoreComInterfaces.SERIAL_DLE.value: "Serial Interace with DLE encoding", - CoreComInterfaces.TCPIP_UDP.value: "TCP/IP with UDP datagrams", - CoreComInterfaces.TCPIP_TCP.value: "TCP/IP with TCP", - CoreComInterfaces.SERIAL_FIXED_FRAME.value: "Serial Interface with fixed size frames", - CoreComInterfaces.SERIAL_QEMU.value: "Serial Interface using QEMU", - CoreComInterfaces.UNSPECIFIED.value: "Unspecified", -} - - -# Mode options, set by args parser -class CoreModeList(enum.IntEnum): - SEQUENTIAL_CMD_MODE = 0 - LISTENER_MODE = 1 - GUI_MODE = 2 - IDLE = 5 - PROMPT_MODE = 6 - CONTINUOUS_MODE = ( - 7 # will start a daemon handling tm and return after sending one tc - ) - - -CoreModeStrings = { - CoreModeList.SEQUENTIAL_CMD_MODE: "seqcmd", - CoreModeList.LISTENER_MODE: "listener", - CoreModeList.GUI_MODE: "gui", -} - - -class CoreServiceList(enum.Enum): - SERVICE_2 = "2" - SERVICE_3 = "3" - SERVICE_5 = "5" - SERVICE_8 = "8" - SERVICE_9 = "9" - SERVICE_11 = "11" - SERVICE_17 = "17" - SERVICE_20 = "20" - SERVICE_23 = "23" - SERVICE_200 = "200" - - -DEFAULT_APID = 0xEF -DEBUG_MODE = False diff --git a/src/tmtccmd/config/hook.py b/src/tmtccmd/config/hook.py deleted file mode 100644 index 8bf52dcd..00000000 --- a/src/tmtccmd/config/hook.py +++ /dev/null @@ -1,114 +0,0 @@ -from abc import abstractmethod -from typing import Optional, Union - -from tmtccmd.config.definitions import ( - ServiceOpCodeDictT, - DataReplyUnpacked, - default_json_path, -) -from tmtccmd.logging import get_console_logger -from tmtccmd.utility.retval import RetvalDictT -from tmtccmd.utility.obj_id import ObjectIdDictT -from tmtccmd.core.backend import BackendBase -from tmtccmd.tc.definitions import TcQueueT -from tmtccmd.com_if.com_interface_base import CommunicationInterface - -LOGGER = get_console_logger() - - -class TmTcHookBase: - """This hook allows users to adapt the TMTC commander core to the unique mission requirements. - It is used by implementing all abstract functions and then passing the instance to the - TMTC commander core. - """ - - def __init__(self, json_cfg_path: Optional[str] = None): - self.json_cfg_path = json_cfg_path - if self.json_cfg_path is None: - self.json_cfg_path = default_json_path() - - @abstractmethod - def get_object_ids(self) -> ObjectIdDictT: - from tmtccmd.config.objects import get_core_object_ids - - """The user can specify an object ID dictionary here mapping object ID bytearrays to a - list. This list could contain containing the string representation or additional - information about that object ID. - """ - return get_core_object_ids() - - @abstractmethod - def assign_communication_interface( - self, com_if_key: str - ) -> Optional[CommunicationInterface]: - """Assign the communication interface used by the TMTC commander to send and receive - TMTC with. - - :param com_if_key: String key of the communication interface to be created. - """ - from tmtccmd.config.com_if import create_communication_interface_default - - return create_communication_interface_default( - com_if_key=com_if_key, json_cfg_path=self.json_cfg_path - ) - - @abstractmethod - def get_service_op_code_dictionary(self) -> ServiceOpCodeDictT: - """This is a dicitonary mapping services represented by strings to an operation code - dictionary. - - :return: - """ - from tmtccmd.config.globals import get_default_service_op_code_dict - - return get_default_service_op_code_dict() - - @abstractmethod - def perform_mode_operation(self, tmtc_backend: BackendBase, mode: int): - """Perform custom mode operations - :param tmtc_backend: - :param mode: - :return: - """ - pass - - @abstractmethod - def pack_service_queue( - self, service: Union[int, str], op_code: str, service_queue: TcQueueT - ): - """Overriding this function allows the user to package a telecommand queue for a given - service and operation code combination. - - :param service: - :param op_code: - :param service_queue: - :return: - """ - pass - - def get_retval_dict(self) -> RetvalDictT: - LOGGER.info("No return value dictionary specified") - return dict() - - -def get_global_hook_obj() -> Optional[TmTcHookBase]: - """This function can be used to get the handle to the global hook object. - :return: - """ - try: - from tmtccmd.core.globals_manager import get_global - from tmtccmd.config.definitions import CoreGlobalIds - - from typing import cast - - hook_obj_raw = get_global(CoreGlobalIds.TMTC_HOOK) - if hook_obj_raw is None: - LOGGER.error("Hook object is invalid!") - return None - return cast(TmTcHookBase, hook_obj_raw) - except ImportError: - LOGGER.exception("Issues importing modules to get global hook handle!") - return None - except AttributeError: - LOGGER.exception("Attribute error when trying to get global hook handle!") - return None diff --git a/src/tmtccmd/config/logo.png b/src/tmtccmd/config/logo.png deleted file mode 100644 index 9441f580..00000000 Binary files a/src/tmtccmd/config/logo.png and /dev/null differ diff --git a/src/tmtccmd/core/__init__.py b/src/tmtccmd/core/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tmtccmd/core/backend.py b/src/tmtccmd/core/backend.py deleted file mode 100644 index c646bca5..00000000 --- a/src/tmtccmd/core/backend.py +++ /dev/null @@ -1,319 +0,0 @@ -import atexit -import time -import sys -from threading import Thread -from abc import abstractmethod -from collections import deque -from typing import Union, cast, Optional, Tuple - -from tmtccmd.config.definitions import CoreServiceList, CoreModeList -from tmtccmd.tm.definitions import TmTypes -from tmtccmd.tm.handler import TmHandler -from tmtccmd.logging import get_console_logger -from tmtccmd.sendreceive.sequential_sender_receiver import ( - SequentialCommandSenderReceiver, - UsrSendCbT, -) -from tmtccmd.sendreceive.tm_listener import TmListener -from tmtccmd.ccsds.handler import CcsdsTmHandler -from tmtccmd.com_if.com_interface_base import CommunicationInterface -from tmtccmd.tc.packer import ServiceQueuePacker - - -LOGGER = get_console_logger() - - -class BackendBase: - @abstractmethod - def initialize(self): - """Initialize the backend. Raise RuntimeError or ValueError on failure""" - - @abstractmethod - def start_listener(self): - """Start the backend. Raise RuntimeError on failure""" - - @abstractmethod - def set_mode(self, mode: int): - """Set backend mode - :param mode: - :return: - """ - - -class TmTcHandler(BackendBase): - """This is the primary class which handles TMTC reception. This can be seen as the backend - in case a GUI or front-end is implemented. - """ - - def __init__( - self, - com_if: CommunicationInterface, - tm_listener: TmListener, - tm_handler: TmHandler, - init_mode: int, - init_service: Union[str, int] = CoreServiceList.SERVICE_17.value, - init_opcode: str = "0", - ): - self.mode = init_mode - self.com_if_key = com_if.get_id() - self.__com_if_active = False - self.__service = init_service - self.__op_code = init_opcode - self.__apid = 0 - self.__usr_send_wrapper: Optional[Tuple[UsrSendCbT, any]] = None - - # This flag could be used later to command the TMTC Client with a front-end - self.one_shot_operation = False - - self.__com_if = com_if - self.__tm_listener = tm_listener - if tm_handler.get_type() == TmTypes.CCSDS_SPACE_PACKETS: - self.__tm_handler: CcsdsTmHandler = cast(CcsdsTmHandler, tm_handler) - for apid_queue_len_tuple in self.__tm_handler.get_apid_queue_len_list(): - self.__tm_listener.subscribe_ccsds_tm_handler( - apid_queue_len_tuple[0], apid_queue_len_tuple[1] - ) - self.exit_on_com_if_init_failure = True - self.single_command_package = bytearray(), None - - # WIP: optionally having a receiver run in the background - self.daemon_receiver = SequentialCommandSenderReceiver( - com_if=self.__com_if, - tm_handler=self.__tm_handler, - tm_listener=self.__tm_listener, - tc_queue=deque(), - apid=self.__apid, - usr_send_wrapper=self.usr_send_wrapper, - ) - - def get_com_if_id(self): - return self.com_if_key - - def get_com_if(self) -> CommunicationInterface: - return self.__com_if - - def get_listener(self): - return self.__tm_listener - - def set_com_if(self, com_if: CommunicationInterface): - if not self.is_com_if_active(): - self.__com_if = com_if - self.__tm_listener.set_com_if(self.__com_if) - else: - LOGGER.warning( - "Communication Interface is active and must be closed first before " - "reassigning a new one" - ) - - @property - def usr_send_wrapper(self): - return self.__usr_send_wrapper - - @usr_send_wrapper.setter - def usr_send_wrapper(self, usr_send_wrapper: UsrSendCbT): - self.__usr_send_wrapper = usr_send_wrapper - - def is_com_if_active(self): - return self.__com_if_active - - def set_mode(self, mode: int): - """ - Set the mode which will determine what perform_operation does. - """ - self.mode = mode - - def get_mode(self) -> int: - return self.mode - - def set_service(self, service: Union[str, int]): - self.__service = service - - def set_opcode(self, op_code: str): - self.__op_code = op_code - - def get_service(self) -> Union[str, int]: - return self.__service - - def get_opcode(self) -> str: - return self.__op_code - - def get_current_apid(self) -> int: - return self.__apid - - def set_current_apid(self, apid: int): - self.__apid = apid - self.daemon_receiver._apid = apid - - @staticmethod - def prepare_tmtc_handler_start( - com_if: CommunicationInterface, - tm_listener: TmListener, - tm_handler: TmHandler, - init_mode: int, - init_service: Union[str, int] = CoreServiceList.SERVICE_17.value, - init_opcode: str = "0", - ): - from multiprocessing import Process - - tmtc_handler = TmTcHandler( - com_if=com_if, - tm_listener=tm_listener, - init_mode=init_mode, - init_service=init_service, - init_opcode=init_opcode, - tm_handler=tm_handler, - ) - tmtc_task = Process(target=TmTcHandler.start_handler, args=(tmtc_handler,)) - return tmtc_task - - @staticmethod - def start_handler(executed_handler): - if not isinstance(executed_handler, TmTcHandler): - LOGGER.error("Unexpected argument, should be TmTcHandler!") - sys.exit(1) - executed_handler.initialize() - executed_handler.start_listener() - - def initialize(self): - from tmtccmd.utility.exit_handler import keyboard_interrupt_handler - - """ - Perform initialization steps which might be necessary after class construction. - This has to be called at some point before using the class! - """ - if self.mode == CoreModeList.LISTENER_MODE: - LOGGER.info("Running in listener mode..") - atexit.register( - keyboard_interrupt_handler, tmtc_backend=self, com_interface=self.__com_if - ) - - def start_listener(self, perform_op_immediately: bool = True): - try: - self.__com_if.open() - self.__tm_listener.start() - self.__com_if_active = True - except IOError: - LOGGER.error("Communication Interface could not be opened!") - LOGGER.info("TM listener will not be started") - if self.exit_on_com_if_init_failure: - LOGGER.error("Closing TMTC commander..") - self.__com_if.close() - sys.exit(1) - if self.mode == CoreModeList.CONTINUOUS_MODE: - self.daemon_receiver.start_daemon() - if perform_op_immediately: - self.perform_operation() - - def close_listener(self, join: bool = False, join_timeout_seconds: float = 1.0): - """Closes the TM listener and the communication interface. This is started in a separarate - thread because the communication interface might still be busy. The completion can be - checked with :meth:`tmtccmd.core.backend.is_com_if_active`. Alternatively, waiting on - completion is possible by specifying the join argument and a timeout in - floating point second. - :param join: - :param join_timeout_seconds: - :return: - """ - if self.__com_if_active: - close_thread = Thread(target=self.__com_if_closing) - close_thread.start() - if join: - close_thread.join(timeout=join_timeout_seconds) - - def perform_operation(self): - """Periodic operation""" - try: - self.__core_operation(self.one_shot_operation) - except KeyboardInterrupt: - LOGGER.info("Keyboard Interrupt.") - sys.exit() - except IOError: - LOGGER.exception("IO Error occured") - sys.exit() - - def __com_if_closing(self): - self.__tm_listener.stop() - while True: - if not self.__tm_listener.is_listener_active(): - self.__com_if.close() - self.__com_if_active = False - break - else: - time.sleep(0.2) - - def start_daemon_receiver(self): - try: - self.daemon_receiver.start_daemon() - except RuntimeError: - LOGGER.error("Error when starting daemon receiver. Not starting it") - except Exception as e: - LOGGER.exception( - f"Unknown exception {e} when starting daemon receiver. Not starting it" - ) - - def __handle_action(self): - """Command handling.""" - if self.mode == CoreModeList.LISTENER_MODE: - if self.__tm_listener.reply_event(): - LOGGER.info("TmTcHandler: Packets received.") - packet_queues = self.__tm_listener.retrieve_tm_packet_queues(clear=True) - if len(packet_queues) > 0: - self.__tm_handler.handle_packet_queues( - packet_queue_list=packet_queues - ) - self.__tm_listener.clear_reply_event() - elif self.mode == CoreModeList.SEQUENTIAL_CMD_MODE: - service_queue = deque() - service_queue_packer = ServiceQueuePacker() - service_queue_packer.pack_service_queue_core( - service=self.__service, - service_queue=service_queue, - op_code=self.__op_code, - ) - if not self.__com_if.valid: - return - LOGGER.info("Performing sequential command operation") - sender_and_receiver = SequentialCommandSenderReceiver( - com_if=self.__com_if, - tm_handler=self.__tm_handler, - tm_listener=self.__tm_listener, - tc_queue=service_queue, - apid=self.__apid, - usr_send_wrapper=self.usr_send_wrapper, - ) - sender_and_receiver.send_queue_tc_and_receive_tm_sequentially() - self.mode = CoreModeList.LISTENER_MODE - elif self.mode == CoreModeList.CONTINUOUS_MODE: - service_queue = deque() - service_queue_packer = ServiceQueuePacker() - service_queue_packer.pack_service_queue_core( - service=self.__service, - service_queue=service_queue, - op_code=self.__op_code, - ) - if not self.__com_if.valid: - return - LOGGER.info("Performing service command operation") - self.daemon_receiver.set_tc_queue(service_queue) - self.daemon_receiver.send_queue_tc_and_return() - else: - try: - from tmtccmd.config.hook import get_global_hook_obj - - hook_obj = get_global_hook_obj() - hook_obj.perform_mode_operation(mode=self.mode, tmtc_backend=self) - except ImportError as error: - print(error) - LOGGER.error("Custom mode handling module not provided!") - - def __core_operation(self, one_shot: bool): - if not one_shot: - while True: - self.__handle_action() - if self.mode == CoreModeList.IDLE: - LOGGER.info("TMTC Client in idle mode") - time.sleep(5) - elif self.mode == CoreModeList.LISTENER_MODE: - time.sleep(1) - else: - self.__handle_action() diff --git a/src/tmtccmd/core/frontend.py b/src/tmtccmd/core/frontend.py deleted file mode 100644 index 73d591de..00000000 --- a/src/tmtccmd/core/frontend.py +++ /dev/null @@ -1,537 +0,0 @@ -#!/usr/bin/env python3 -""" -@file tmtc_frontend.py -@date 01.11.2019 -@brief This is part of the TMTC client developed by the SOURCE project by KSat -@description GUI is still work-in-progress -@manual -@author R. Mueller, P. Scheurenbrand, D. Nguyen -""" -import enum -import os -import sys -import time -import webbrowser -from multiprocessing import Process -from typing import Union - -from PyQt5.QtWidgets import ( - QMainWindow, - QGridLayout, - QTableWidget, - QWidget, - QLabel, - QCheckBox, - QDoubleSpinBox, - QFrame, - QComboBox, - QPushButton, - QTableWidgetItem, - QMenu, - QAction, - QMenuBar, -) -from PyQt5.QtGui import QPixmap, QIcon -from PyQt5.QtCore import Qt, pyqtSignal, QObject, QThread, QRunnable - -from tmtccmd.core.frontend_base import FrontendBase -from tmtccmd.core.backend import TmTcHandler -from tmtccmd.config.hook import TmTcHookBase -from tmtccmd.config.definitions import CoreGlobalIds, CoreModeList, CoreComInterfaces -from tmtccmd.config.hook import get_global_hook_obj -from tmtccmd.logging import get_console_logger -from tmtccmd.core.globals_manager import get_global, update_global -from tmtccmd.com_if.tcpip_utilities import TcpIpConfigIds -import tmtccmd.config as config_module - - -LOGGER = get_console_logger() - - -CONNECT_BTTN_STYLE = ( - "background-color: #1fc600;" - "border-style: inset;" - "font: bold;" - "padding: 6px;" - "border-width: 2px;" - "border-radius: 6px;" -) - - -DISCONNECT_BTTN_STYLE = ( - "background-color: orange;" - "border-style: inset;" - "font: bold;" - "padding: 6px;" - "border-width: 2px;" - "border-radius: 6px;" -) - - -COMMAND_BUTTON_STYLE = ( - "background-color: #cdeefd;" - "border-style: inset;" - "font: bold;" - "padding: 6px;" - "border-width: 2px;" - "border-radius: 6px;" -) - - -class WorkerOperationsCodes(enum.IntEnum): - DISCONNECT = 0 - SEQUENTIAL_COMMANDING = 1 - LISTENING = 2 - IDLE = 4 - - -class WorkerThread(QObject): - disconnected = pyqtSignal() - command_executed = pyqtSignal() - - def __init__(self, op_code: WorkerOperationsCodes, tmtc_handler: TmTcHandler): - super(QObject, self).__init__() - self.op_code = op_code - self.tmtc_handler = tmtc_handler - self.tmtc_handler.one_shot_operation = True - - def set_op_code(self, op_code: WorkerOperationsCodes): - self.op_code = op_code - - def run_worker(self): - while True: - op_code = self.op_code - if op_code == WorkerOperationsCodes.DISCONNECT: - self.tmtc_handler.close_listener() - while True: - if not self.tmtc_handler.is_com_if_active(): - break - else: - time.sleep(0.4) - self.op_code = WorkerOperationsCodes.IDLE - self.disconnected.emit() - elif op_code == WorkerOperationsCodes.SEQUENTIAL_COMMANDING: - self.tmtc_handler.one_shot_operation = True - # It is expected that the TMTC handler is in the according state to perform the - # operation - self.tmtc_handler.perform_operation() - self.op_code = WorkerOperationsCodes.LISTENING - self.command_executed.emit() - elif op_code == WorkerOperationsCodes.LISTENING: - self.tmtc_handler.one_shot_operation = True - self.tmtc_handler.set_mode(CoreModeList.LISTENER_MODE) - self.tmtc_handler.perform_operation() - elif op_code == WorkerOperationsCodes.IDLE: - pass - else: - # This must be a programming error - LOGGER.error("Unknown worker operation code {0}!".format(self.op_code)) - - -class RunnableThread(QRunnable): - """ - Runnable thread which can be used with QThreadPool. Not used for now, might be needed in the future. - """ - - def run(self): - pass - - -class TmTcFrontend(QMainWindow, FrontendBase): - def __init__( - self, hook_obj: TmTcHookBase, tmtc_backend: TmTcHandler, app_name: str - ): - super(TmTcFrontend, self).__init__() - super(QMainWindow, self).__init__() - self._tmtc_handler = tmtc_backend - self._app_name = app_name - self._hook_obj = hook_obj - - self._tmtc_handler.initialize() - self.service_op_code_dict = dict() - self._service_list = [] - self._op_code_list = [] - self._com_if_list = [] - self._last_com_if = CoreComInterfaces.UNSPECIFIED.value - self._current_com_if = CoreComInterfaces.UNSPECIFIED.value - self._current_service = "" - self._current_op_code = "" - self._current_com_if_key = "unspec" - self.__connected = False - - self.__worker = None - self.__thread = None - self.__debug_mode = True - - self.__combo_box_op_codes: Union[None, QComboBox] = None - module_path = os.path.abspath(config_module.__file__).replace("__init__.py", "") - self.logo_path = f"{module_path}/logo.png" - self.__start_qthread_task(WorkerOperationsCodes.LISTENING) - - def prepare_start(self, args: any) -> Process: - return Process(target=self.start) - - def start(self, qt_app: any): - self.__start_ui() - sys.exit(qt_app.exec()) - - def set_gui_logo(self, logo_total_path: str): - if os.path.isfile(logo_total_path): - self.logo_path = logo_total_path - else: - LOGGER.warning("Could not set logo, path invalid!") - - def __start_ui(self): - self.__create_menu_bar() - win = QWidget(self) - self.setCentralWidget(win) - - grid = QGridLayout() - win.setLayout(grid) - row = 0 - self.setWindowTitle(self._app_name) - self.setWindowIcon(QIcon(self.logo_path)) - - add_pixmap = False - - if add_pixmap: - row = self.__set_up_pixmap(grid=grid, row=row) - - row = self.__set_up_config_section(grid=grid, row=row) - row = self.__add_vertical_separator(grid=grid, row=row) - - # com if configuration - row = self.__set_up_com_if_section(grid=grid, row=row) - row = self.__add_vertical_separator(grid=grid, row=row) - - row = self.__set_up_service_op_code_section(grid=grid, row=row) - - self.__command_button = QPushButton() - self.__command_button.setText("Send Command") - self.__command_button.setStyleSheet(COMMAND_BUTTON_STYLE) - self.__command_button.clicked.connect(self.__start_seq_cmd_op) - self.__command_button.setEnabled(False) - grid.addWidget(self.__command_button, row, 0, 1, 2) - row += 1 - self.show() - - def __start_seq_cmd_op(self): - if self.__debug_mode: - LOGGER.info("Send command button pressed.") - if not self.__get_send_button(): - return - self.__set_send_button(False) - self._tmtc_handler.set_service(self._current_service) - self._tmtc_handler.set_opcode(self._current_op_code) - self._tmtc_handler.set_mode(CoreModeList.SEQUENTIAL_CMD_MODE) - self.__worker.set_op_code(WorkerOperationsCodes.SEQUENTIAL_COMMANDING) - self.__worker.command_executed.connect(self.__finish_seq_cmd_op) - - def __finish_seq_cmd_op(self): - self.__set_send_button(True) - - def __connect_button_action(self): - if not self.__connected: - LOGGER.info("Starting TM listener..") - # Build and assign new communication interface - if self._current_com_if != self._last_com_if: - hook_obj = get_global_hook_obj() - new_com_if = hook_obj.assign_communication_interface( - com_if_key=self._current_com_if - ) - self._last_com_if = self._current_com_if - self._tmtc_handler.set_com_if(new_com_if) - LOGGER.info("Starting listener") - self._tmtc_handler.start_listener(False) - self.__connect_button.setStyleSheet(DISCONNECT_BTTN_STYLE) - self.__command_button.setEnabled(True) - self.__connect_button.setText("Disconnect") - self.__connected = True - else: - LOGGER.info("Closing TM listener..") - self.__command_button.setEnabled(False) - self.__connect_button.setEnabled(False) - self.__worker.set_op_code(WorkerOperationsCodes.DISCONNECT) - - def __finish_disconnect_button_op(self): - self.__connect_button.setEnabled(True) - # self.__disconnect_button.setEnabled(False) - self.__connect_button.setStyleSheet(CONNECT_BTTN_STYLE) - self.__connect_button.setText("Connect") - LOGGER.info("Disconnect successfull") - self.__connected = False - - def __create_menu_bar(self): - menu_bar = self.menuBar() - # Creating menus using a QMenu object - file_menu = QMenu("&File", self) - menu_bar.addMenu(file_menu) - # Creating menus using a title - help_menu = menu_bar.addMenu("&Help") - - help_action = QAction("Help", self) - help_action.triggered.connect(self.__help_url) - help_menu.addAction(help_action) - - @staticmethod - def __help_url(): - webbrowser.open("https://tmtccmd.readthedocs.io/en/latest/") - - def __set_up_config_section(self, grid: QGridLayout, row: int) -> int: - grid.addWidget(QLabel("Configuration:"), row, 0, 1, 2) - row += 1 - checkbox_console = QCheckBox("Print output to console") - checkbox_console.stateChanged.connect(self.__checkbox_console_update) - - checkbox_log = QCheckBox("Print output to log file") - checkbox_log.stateChanged.connect(self.__checkbox_log_update) - - checkbox_raw_tm = QCheckBox("Print all raw TM data directly") - checkbox_raw_tm.stateChanged.connect(self.__checkbox_print_raw_data_update) - - checkbox_hk = QCheckBox("Print Housekeeping Data") - # checkbox_hk.setChecked(tmtcc_config.G_PRINT_HK_DATA) - checkbox_hk.stateChanged.connect(checkbox_print_hk_data) - - checkbox_short = QCheckBox("Short Display Mode") - # checkbox_short.setChecked(tmtcc_config.G_DISPLAY_MODE == "short") - checkbox_short.stateChanged.connect(checkbox_short_display_mode) - - grid.addWidget(checkbox_log, row, 0, 1, 1) - grid.addWidget(checkbox_console, row, 1, 1, 1) - row += 1 - grid.addWidget(checkbox_raw_tm, row, 0, 1, 1) - grid.addWidget(checkbox_hk, row, 1, 1, 1) - row += 1 - grid.addWidget(checkbox_short, row, 0, 1, 1) - row += 1 - - grid.addWidget(QLabel("TM Timeout:"), row, 0, 1, 1) - grid.addWidget(QLabel("TM Timeout Factor:"), row, 1, 1, 1) - row += 1 - - spin_timeout = QDoubleSpinBox() - spin_timeout.setValue(4) - # TODO: set sensible min/max values - spin_timeout.setSingleStep(0.1) - spin_timeout.setMinimum(0.25) - spin_timeout.setMaximum(60) - # https://youtrack.jetbrains.com/issue/PY-22908 - # Ignore those warnings for now. - spin_timeout.valueChanged.connect(number_timeout) - grid.addWidget(spin_timeout, row, 0, 1, 1) - - spin_timeout_factor = QDoubleSpinBox() - # spin_timeout_factor.setValue(tmtcc_config.G_TC_SEND_TIMEOUT_FACTOR) - # TODO: set sensible min/max values - spin_timeout_factor.setSingleStep(0.1) - spin_timeout_factor.setMinimum(0.25) - spin_timeout_factor.setMaximum(10) - spin_timeout_factor.valueChanged.connect(number_timeout_factor) - grid.addWidget(spin_timeout_factor, row, 1, 1, 1) - row += 1 - return row - - def __set_up_com_if_section(self, grid: QGridLayout, row: int) -> int: - grid.addWidget(QLabel("Communication Interface:"), row, 0, 1, 1) - com_if_combo_box = QComboBox() - all_com_ifs = get_global(CoreGlobalIds.COM_IF_DICT) - index = 0 - # add all possible ComIFs to the comboBox - for com_if_key, com_if_value in all_com_ifs.items(): - com_if_combo_box.addItem(com_if_value) - self._com_if_list.append((com_if_key, com_if_value)) - if self._tmtc_handler.get_com_if_id() == com_if_key: - com_if_combo_box.setCurrentIndex(index) - index += 1 - com_if_combo_box.currentIndexChanged.connect(self.__com_if_sel_index_changed) - grid.addWidget(com_if_combo_box, row, 1, 1, 1) - row += 1 - - self.com_if_cfg_button = QPushButton() - self.com_if_cfg_button.setText("Configure") - grid.addWidget(self.com_if_cfg_button, row, 0, 1, 2) - row += 1 - - self.__connect_button = QPushButton() - self.__connect_button.setText("Connect") - self.__connect_button.setStyleSheet(CONNECT_BTTN_STYLE) - self.__connect_button.clicked.connect(self.__connect_button_action) - - grid.addWidget(self.__connect_button, row, 0, 1, 2) - row += 1 - return row - - def __set_up_service_op_code_section(self, grid: QGridLayout, row: int): - grid.addWidget(QLabel("Service: "), row, 0, 1, 2) - grid.addWidget(QLabel("Operation Code: "), row, 1, 1, 2) - row += 1 - - combo_box_services = QComboBox() - default_service = get_global(CoreGlobalIds.CURRENT_SERVICE) - self.service_op_code_dict = self._hook_obj.get_service_op_code_dictionary() - if self.service_op_code_dict is None: - LOGGER.warning("Invalid service to operation code dictionary") - LOGGER.warning("Setting default dictionary") - from tmtccmd.config.globals import get_default_service_op_code_dict - - self.service_op_code_dict = get_default_service_op_code_dict() - index = 0 - default_index = 0 - for service_key, service_value in self.service_op_code_dict.items(): - combo_box_services.addItem(service_value[0]) - if service_key == default_service: - default_index = index - self._service_list.append(service_key) - index += 1 - combo_box_services.setCurrentIndex(default_index) - self._current_service = self._service_list[default_index] - - combo_box_services.currentIndexChanged.connect(self.__service_index_changed) - grid.addWidget(combo_box_services, row, 0, 1, 1) - - self.__combo_box_op_codes = QComboBox() - self._current_service = self._service_list[default_index] - self.__update_op_code_combo_box() - self.__combo_box_op_codes.currentIndexChanged.connect( - self.__op_code_index_changed - ) - # TODO: Combo box also needs to be updated if another service is selected - grid.addWidget(self.__combo_box_op_codes, row, 1, 1, 1) - row += 1 - return row - - def __set_up_pixmap(self, grid: QGridLayout, row: int) -> int: - label = QLabel(self) - label.setGeometry(720, 10, 100, 100) - label.adjustSize() - - pixmap = QPixmap(self.logo_path) - pixmap_width = pixmap.width() - pixmap_height = pixmap.height() - row += 1 - - pixmap_scaled = pixmap.scaled( - pixmap_width * 0.3, pixmap_height * 0.3, Qt.KeepAspectRatio - ) - label.setPixmap(pixmap_scaled) - label.setScaledContents(True) - - grid.addWidget(label, row, 0, 1, 2) - row += 1 - return row - - def __start_qthread_task(self, op_code: WorkerOperationsCodes): - self.__thread = QThread() - self.__worker = WorkerThread(op_code=op_code, tmtc_handler=self._tmtc_handler) - self.__worker.moveToThread(self.__thread) - self.__thread.started.connect(self.__worker.run_worker) - self.__thread.start() - self.__worker.disconnected.connect(self.__finish_disconnect_button_op) - - @staticmethod - def __add_vertical_separator(grid: QGridLayout, row: int): - separator = QFrame() - separator.setFrameShape(QFrame.HLine) - grid.addWidget(separator, row, 0, 1, 2) - row += 1 - return row - - def __service_index_changed(self, index: int): - self._current_service = self._service_list[index] - self.__update_op_code_combo_box() - if self.__debug_mode: - LOGGER.info("Service changed") - - def __op_code_index_changed(self, index: int): - self._current_op_code = self._op_code_list[index] - if self.__debug_mode: - LOGGER.info("Op Code changed") - - def __update_op_code_combo_box(self): - self.__combo_box_op_codes.clear() - self._op_code_list = [] - op_code_dict = self.service_op_code_dict[self._current_service][1] - if op_code_dict is not None: - for op_code_key, op_code_value in op_code_dict.items(): - try: - self._op_code_list.append(op_code_key) - self.__combo_box_op_codes.addItem(op_code_value[0]) - except TypeError: - LOGGER.warning(f"Invalid op code entry {op_code_value}, skipping..") - self._current_op_code = self._op_code_list[0] - - def __checkbox_log_update(self, state: int): - update_global(CoreGlobalIds.PRINT_TO_FILE, state) - if self.__debug_mode: - LOGGER.info(["Enabled", "Disabled"][state == 0] + " print to log.") - - def __checkbox_console_update(self, state: bool): - update_global(CoreGlobalIds.PRINT_TM, state) - if self.__debug_mode: - LOGGER.info(["enabled", "disabled"][state == 0] + " console print") - - def __checkbox_print_raw_data_update(self, state: int): - update_global(CoreGlobalIds.PRINT_RAW_TM, state) - if self.__debug_mode: - LOGGER.info(["enabled", "disabled"][state == 0] + " printing of raw data") - - def __set_send_button(self, state: bool): - self.__command_button.setEnabled(state) - - def __get_send_button(self): - return self.__command_button.isEnabled() - - def __com_if_sel_index_changed(self, index: int): - self._current_com_if = self._com_if_list[index][0] - if self.__debug_mode: - LOGGER.info(f"Communication IF updated: {self._com_if_list[index][1]}") - - -class SingleCommandTable(QTableWidget): - def __init__(self): - super().__init__() - self.setRowCount(1) - self.setColumnCount(5) - self.setHorizontalHeaderItem(0, QTableWidgetItem("Service")) - self.setHorizontalHeaderItem(1, QTableWidgetItem("Subservice")) - self.setHorizontalHeaderItem(2, QTableWidgetItem("SSC")) - self.setHorizontalHeaderItem(3, QTableWidgetItem("Data")) - self.setHorizontalHeaderItem(4, QTableWidgetItem("CRC")) - self.setItem(0, 0, QTableWidgetItem("17")) - self.setItem(0, 1, QTableWidgetItem("1")) - self.setItem(0, 2, QTableWidgetItem("20")) - - -def checkbox_print_hk_data(state: int): - update_global(CoreGlobalIds.PRINT_HK, state) - LOGGER.info(["enabled", "disabled"][state == 0] + " printing of hk data") - - -def checkbox_short_display_mode(state: int): - update_global(CoreGlobalIds.DISPLAY_MODE, state) - LOGGER.info(["enabled", "disabled"][state == 0] + " short display mode") - - -def number_timeout(value: float): - update_global(CoreGlobalIds.TM_TIMEOUT, value) - LOGGER.info("PUS TM timeout changed to: " + str(value)) - - -def number_timeout_factor(value: float): - update_global(CoreGlobalIds.TC_SEND_TIMEOUT_FACTOR, value) - LOGGER.info("PUS TM timeout factor changed to: " + str(value)) - - -def ip_change_client(value): - ethernet_config = get_global(CoreGlobalIds.ETHERNET_CONFIG) - ethernet_config[TcpIpConfigIds.RECV_ADDRESS] = value - update_global(CoreGlobalIds.ETHERNET_CONFIG, ethernet_config) - LOGGER.info("Client IP changed: " + value) - - -def ip_change_board(value): - ethernet_config = get_global(CoreGlobalIds.ETHERNET_CONFIG) - ethernet_config[TcpIpConfigIds.SEND_ADDRESS] = value - update_global(CoreGlobalIds.ETHERNET_CONFIG, ethernet_config) - LOGGER.info("Board IP changed: " + value) diff --git a/src/tmtccmd/core/object_id_manager.py b/src/tmtccmd/core/object_id_manager.py deleted file mode 100644 index d6d8d7c9..00000000 --- a/src/tmtccmd/core/object_id_manager.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Dict - -from tmtccmd.logging import get_console_logger - -LOGGER = get_console_logger() -__OBJECT_ID_DICT = dict() - - -def insert_object_id(object_id: bytes, object_id_info: list): - __OBJECT_ID_DICT[object_id] = object_id_info - - -def insert_object_ids(object_id_dict: Dict[bytes, list]): - if object_id_dict is not None: - __OBJECT_ID_DICT.update(object_id_dict) - - -def get_object_id_info(object_id: bytes): - return __OBJECT_ID_DICT.get(object_id) diff --git a/src/tmtccmd/logging/pus.py b/src/tmtccmd/logging/pus.py deleted file mode 100644 index b48e701d..00000000 --- a/src/tmtccmd/logging/pus.py +++ /dev/null @@ -1,121 +0,0 @@ -import logging -import os -from typing import Optional, Tuple -from datetime import datetime -from tmtccmd.logging import LOG_DIR -from spacepackets.ccsds.spacepacket import PacketTypes -from logging.handlers import RotatingFileHandler -from logging import FileHandler - -RAW_PUS_FILE_BASE_NAME = "pus-log" -RAW_PUS_LOGGER_NAME = "pus-log" - -TMTC_FILE_BASE_NAME = "tmtc-log" -TMTC_LOGGER_NAME = "tmtc-log" - -__TMTC_LOGGER: Optional[logging.Logger] = None -__RAW_PUS_LOGGER: Optional[logging.Logger] = None - - -def create_raw_pus_file_logger(max_bytes: int = 8192 * 16) -> logging.Logger: - """Create a logger to log raw PUS messages by returning a rotating file handler which has - the current date in its log file name. This function is not thread-safe. - :return: - """ - global __RAW_PUS_LOGGER - file_name = get_current_raw_file_name() - if __RAW_PUS_LOGGER is None: - __RAW_PUS_LOGGER = logging.getLogger(RAW_PUS_LOGGER_NAME) - handler = RotatingFileHandler( - filename=file_name, maxBytes=max_bytes, backupCount=10 - ) - formatter = logging.Formatter( - fmt="%(asctime)s.%(msecs)03d: %(message)s", datefmt="%Y-%m-%d %H:%M:%S" - ) - handler.setFormatter(fmt=formatter) - __RAW_PUS_LOGGER.addHandler(handler) - __RAW_PUS_LOGGER.setLevel(logging.INFO) - __RAW_PUS_LOGGER.info( - f"tmtccmd started at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - ) - return __RAW_PUS_LOGGER - - -def get_current_raw_file_name() -> str: - return f"{LOG_DIR}/{RAW_PUS_FILE_BASE_NAME}_{datetime.now().date()}.log" - - -def get_current_tmtc_file_name() -> str: - return ( - f"{LOG_DIR}/{TMTC_FILE_BASE_NAME}_{datetime.now().date()}_" - f"{datetime.now().time().strftime('%H%M%S')}.log" - ) - - -def log_raw_pus_tc(packet: bytes, srv_subservice: Optional[Tuple[int, int]] = None): - global __RAW_PUS_LOGGER - if __RAW_PUS_LOGGER is None: - __RAW_PUS_LOGGER = create_raw_pus_file_logger() - type_str = "TC" - if srv_subservice is not None: - type_str += f" [{srv_subservice[0], srv_subservice[1]}" - - logged_msg = f"{type_str} | hex [{packet.hex(sep=',')}]" - __RAW_PUS_LOGGER.info(logged_msg) - - -def log_raw_pus_tm(packet: bytes, srv_subservice: Optional[Tuple[int, int]] = None): - global __RAW_PUS_LOGGER - if __RAW_PUS_LOGGER is None: - __RAW_PUS_LOGGER = create_raw_pus_file_logger() - type_str = "TM" - if srv_subservice is not None: - type_str += f" [{srv_subservice[0], srv_subservice[1]}" - - logged_msg = f"{type_str} | hex [{packet.hex(sep=',')}]" - __RAW_PUS_LOGGER.info(logged_msg) - - -def log_raw_unknown_packet(packet: bytes, packet_type: PacketTypes): - global __RAW_PUS_LOGGER - if __RAW_PUS_LOGGER is None: - __RAW_PUS_LOGGER = create_raw_pus_file_logger() - if packet_type == PacketTypes.TC: - type_str = "Unknown TC Packet" - else: - type_str = "Unknown TM Packet" - logged_msg = f"{type_str} | hex [{packet.hex(sep=',')}]" - __RAW_PUS_LOGGER.info(logged_msg) - - -def create_tmtc_logger(): - """Create a generic TMTC logger which logs both to a unique file for a TMTC session. - This functions is not thread-safe. - :return: - """ - global __TMTC_LOGGER - if not os.path.exists(LOG_DIR): - os.mkdir(LOG_DIR) - # This should create a unique event log file for most cases. If for some reason this is called - # with the same name, the events will appended to an old file which was created in the same - # second. This is okay. - file_name = get_current_tmtc_file_name() - if __TMTC_LOGGER is None: - __TMTC_LOGGER = logging.getLogger(TMTC_LOGGER_NAME) - file_handler = FileHandler(filename=file_name) - formatter = logging.Formatter() - file_handler.setFormatter(fmt=formatter) - __TMTC_LOGGER.addHandler(file_handler) - __TMTC_LOGGER.setLevel(logging.INFO) - return __TMTC_LOGGER - - -def get_tmtc_file_logger() -> logging.Logger: - """Returns a generic TMTC logger which logs both to a unique file for a TMTC session. - This functions is not thread-safe. - :return: - """ - global __TMTC_LOGGER - if __TMTC_LOGGER is None: - __TMTC_LOGGER = create_tmtc_logger() - return __TMTC_LOGGER diff --git a/src/tmtccmd/pus/__init__.py b/src/tmtccmd/pus/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tmtccmd/pus/definitions.py b/src/tmtccmd/pus/definitions.py deleted file mode 100644 index e61e1616..00000000 --- a/src/tmtccmd/pus/definitions.py +++ /dev/null @@ -1,6 +0,0 @@ -from enum import IntEnum -from spacepackets.ecss.definitions import PusServices - - -class CustomPusServices(IntEnum): - SERVICE_200_MODE = 200 diff --git a/src/tmtccmd/pus/pus_17_test.py b/src/tmtccmd/pus/pus_17_test.py deleted file mode 100644 index 71f9df01..00000000 --- a/src/tmtccmd/pus/pus_17_test.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations -import enum - -from spacepackets.ecss.conf import get_default_tc_apid -from spacepackets.ecss.pus_17_test import Subservices -from tmtccmd.config.definitions import QueueCommands -from tmtccmd.tc.definitions import PusTelecommand, TcQueueT - - -class CustomSubservices(enum.IntEnum): - TC_GEN_EVENT = 128 - - -def pack_service_17_ping_command(ssc: int, apid: int = -1) -> PusTelecommand: - """Generate a simple ping PUS telecommand packet""" - if apid == -1: - apid = get_default_tc_apid() - return PusTelecommand( - service=17, subservice=Subservices.TC_PING.value, ssc=ssc, apid=apid - ) - - -def pack_generic_service17_test( - init_ssc: int, tc_queue: TcQueueT, apid: int = -1 -) -> int: - if apid == -1: - apid = get_default_tc_apid() - new_ssc = init_ssc - tc_queue.appendleft((QueueCommands.PRINT, "Testing Service 17")) - # ping test - tc_queue.appendleft((QueueCommands.PRINT, "Testing Service 17: Ping Test")) - tc_queue.appendleft(pack_service_17_ping_command(ssc=new_ssc).pack_command_tuple()) - new_ssc += 1 - # enable event - tc_queue.appendleft((QueueCommands.PRINT, "Testing Service 17: Enable Event")) - command = PusTelecommand(service=5, subservice=5, ssc=new_ssc, apid=apid) - tc_queue.appendleft(command.pack_command_tuple()) - new_ssc += 1 - # test event - tc_queue.appendleft((QueueCommands.PRINT, "Testing Service 17: Trigger event")) - command = PusTelecommand( - service=17, subservice=CustomSubservices.TC_GEN_EVENT, ssc=new_ssc, apid=apid - ) - tc_queue.appendleft(command.pack_command_tuple()) - new_ssc += 1 - # invalid subservice - tc_queue.appendleft((QueueCommands.PRINT, "Testing Service 17: Invalid subservice")) - command = PusTelecommand(service=17, subservice=243, ssc=new_ssc, apid=apid) - tc_queue.appendleft(command.pack_command_tuple()) - new_ssc += 1 - return new_ssc diff --git a/src/tmtccmd/pus/pus_20_params.py b/src/tmtccmd/pus/pus_20_params.py deleted file mode 100644 index 647fd7a9..00000000 --- a/src/tmtccmd/pus/pus_20_params.py +++ /dev/null @@ -1,72 +0,0 @@ -import enum - - -class EcssPtc(enum.IntEnum): - BOOLEAN = 1 - ENUMERATED = 2 - UNSIGNED = 3 - SIGNED = 4 - # Float or double values - REAL = 5 - BIT_STRING = 6 - OCTET_STRING = 7 - CHARACTER_STRING = 8 - ABSOLUTE_TIME = 9 - RELATIVE_TIME = 10 - DEDUCED = 11 - PACKET = 12 - - -class EcssPfcUnsigned(enum.IntEnum): - FOUR_BIT = 0 - FIVE_BIT = 1 - SIX_BIT = 2 - SEVEN_BIT = 3 - ONE_BYTE = 4 - NINE_BIT = 5 - TEN_BIT = 6 - ELEVEN_BIT = 7 - TWELVE_BIT = 8 - THIRTEEN_BIT = 9 - FOURTEEN_BIT = 10 - FIFTEEN_BIT = 11 - TWO_BYTES = 12 - THREE_BYTES = 13 - FOUR_BYTES = 14 - SIX_BYTES = 15 - EIGHT_BYTES = 16 - ONE_BIT = 17 - TWO_BIT = 18 - THREE_BIT = 19 - - -class EcssPfcSigned(enum.IntEnum): - FOUR_BIT = 0 - FIVE_BIT = 1 - SIX_BIT = 2 - SEVEN_BIT = 3 - ONE_BYTE = 4 - NINE_BIT = 5 - TEN_BIT = 6 - ELEVEN_BIT = 7 - TWELVE_BIT = 8 - THIRTEEN_BIT = 9 - FOURTEEN_BIT = 10 - FIFTEEN_BIT = 11 - TWO_BYTES = 12 - THREE_BYTES = 13 - FOUR_BYTES = 14 - SIX_BYTES = 15 - EIGHT_BYTES = 16 - - -class EcssPfcReal(enum.IntEnum): - FLOAT_SIMPLE_PRECISION_IEEE = 1 - DOUBLE_PRECISION_IEEE = 2 - FLOAT_PRECISION_MIL_STD_4_OCTETS = 3 - DOUBLE_PRECISION_MIL_STD_6_OCTETS = 4 - - -class CustomSubservices(enum.IntEnum): - LOAD = 128 - DUMP = 129 diff --git a/src/tmtccmd/runner.py b/src/tmtccmd/runner.py deleted file mode 100644 index 566426e3..00000000 --- a/src/tmtccmd/runner.py +++ /dev/null @@ -1,231 +0,0 @@ -"""Contains core methods called by entry point files to setup and start a tmtccmd application""" -import sys -import os -from typing import Union - -from spacepackets.ecss.conf import get_default_tc_apid - -from tmtccmd import __version__ -from tmtccmd.config import SetupArgs, TmTcHookBase, CoreGlobalIds, pass_cli_args -from tmtccmd.core.backend import BackendBase -from tmtccmd.core.frontend_base import FrontendBase -from tmtccmd.tm.definitions import TmTypes -from tmtccmd.tm.handler import TmHandler -from tmtccmd.ccsds.handler import CcsdsTmHandler -from tmtccmd.core.globals_manager import ( - update_global, - get_global, - lock_global_pool, - unlock_global_pool, -) -from tmtccmd.logging import get_console_logger -from .config.globals import set_default_globals_pre_args_parsing - -LOGGER = get_console_logger() - -__SETUP_WAS_CALLED = False -__SETUP_FOR_GUI = False - - -def version() -> str: - return __version__ - - -def add_ccsds_handler(ccsds_handler: CcsdsTmHandler): - """Add a handler for CCSDS space packets, for example PUS packets - - :param ccsds_handler: CCSDS handler for all CCSDS packets, e.g. Space Packets - :return: - """ - lock_global_pool() - tm_handler = get_global(CoreGlobalIds.TM_HANDLER_HANDLE) - if tm_handler is None: - update_global(CoreGlobalIds.TM_HANDLER_HANDLE, ccsds_handler) - unlock_global_pool() - - -def setup(setup_args: SetupArgs): - """This function needs to be called first before running the TMTC commander core. The setup - arguments encapsulate all required arguments for the TMTC commander. - - :param setup_args: Setup arguments - """ - global __SETUP_WAS_CALLED, __SETUP_FOR_GUI - - if os.name == "nt": - import colorama - - colorama.init() - - __assign_tmtc_commander_hooks(hook_object=setup_args.hook_obj) - - if setup_args.use_gui: - set_default_globals_pre_args_parsing( - setup_args.use_gui, tc_apid=setup_args.tc_apid, tm_apid=setup_args.tm_apid - ) - if not setup_args.use_gui: - __handle_cli_args_and_globals(setup_args) - __SETUP_FOR_GUI = setup_args.use_gui - __SETUP_WAS_CALLED = True - - -def run( - tmtc_backend: BackendBase, - tmtc_frontend: Union[FrontendBase, None] = None, - app_name: str = "TMTC Commander", -): - """This is the primary function to run the TMTC commander. Users should call this function to - start the TMTC commander. Please note that :py:func:`setup` needs to be - called before this function. You also need to build a TMTC backend - instance and pass it to this call. You can use :py:func:`create_default_tmtc_backend` - to create a generic backend. - - :param tmtc_backend: Custom backend can be passed here. Otherwise, a default backend - will be created - :param tmtc_frontend: Custom frontend can be passed here. Otherwise, a default frontend - will be created - :param app_name: Name of application. Will be displayed in GUI - :raises RunTimeError: if :py:func:`setup` was not called before - :return: - """ - global __SETUP_WAS_CALLED, __SETUP_FOR_GUI - if not __SETUP_WAS_CALLED: - LOGGER.warning("setup_tmtccmd was not called first. Call it first") - sys.exit(1) - if __SETUP_FOR_GUI: - __start_tmtc_commander_qt_gui( - tmtc_frontend=tmtc_frontend, tmtc_backend=tmtc_backend, app_name=app_name - ) - else: - __start_tmtc_commander_cli(tmtc_backend=tmtc_backend) - - -def init_and_start_daemons(tmtc_backend: BackendBase): - if __SETUP_FOR_GUI: - LOGGER.error("daemon mode only supported in cli mode") - sys.exit(1) - __start_tmtc_commander_cli(tmtc_backend=tmtc_backend, perform_op_immediately=False) - - -def __assign_tmtc_commander_hooks(hook_object: TmTcHookBase): - if hook_object is None: - raise ValueError - # Insert hook object handle into global dictionary so it can be used by the TMTC commander - update_global(CoreGlobalIds.TMTC_HOOK, hook_object) - # TODO: Maybe this is not required anymore.. - # Set core object IDs - # insert_object_ids(get_core_object_ids()) - # Set object IDs specified by the user. - # insert_object_ids(hook_object.get_object_ids()) - - -def init_printout(use_gui: bool, ansi_colors: bool = True): - if ansi_colors: - print(f"-- Python TMTC Commander --") - if use_gui: - print("-- GUI mode --") - else: - print("-- Command line mode --") - - print(f"-- tmtccmd version v{version()} --") - LOGGER.info("Starting TMTC Commander..") - - -def __handle_cli_args_and_globals(setup_args: SetupArgs): - LOGGER.info("Setting up pre-globals..") - set_default_globals_pre_args_parsing( - setup_args.use_gui, tc_apid=setup_args.tc_apid, tm_apid=setup_args.tm_apid - ) - LOGGER.info("Setting up post-globals..") - pass_cli_args(setup_args=setup_args) - - -def __start_tmtc_commander_cli( - tmtc_backend: BackendBase, perform_op_immediately: bool = True -): - __get_backend_init_variables() - tmtc_backend.initialize() - tmtc_backend.start_listener(perform_op_immediately) - - -def __start_tmtc_commander_qt_gui( - tmtc_backend: BackendBase, - tmtc_frontend: Union[None, FrontendBase] = None, - app_name: str = "TMTC Commander", -): - global __SETUP_WAS_CALLED - try: - from PyQt5.QtWidgets import QApplication - - if not __SETUP_WAS_CALLED: - LOGGER.warning("setup_tmtccmd was not called first. Call it first") - sys.exit(1) - app = None - app = QApplication([app_name]) - if tmtc_frontend is None: - from tmtccmd.core.frontend import TmTcFrontend - from tmtccmd.config.hook import get_global_hook_obj - - tmtc_frontend = TmTcFrontend( - hook_obj=get_global_hook_obj(), - tmtc_backend=tmtc_backend, - app_name=app_name, - ) - tmtc_frontend.start(app) - except ImportError: - LOGGER.error("PyQt5 module not installed, can't run GUI mode!") - sys.exit(1) - - -def __get_backend_init_variables(): - service = get_global(CoreGlobalIds.CURRENT_SERVICE) - op_code = get_global(CoreGlobalIds.OP_CODE) - com_if = get_global(CoreGlobalIds.COM_IF) - mode = get_global(CoreGlobalIds.MODE) - return service, op_code, com_if, mode - - -def create_default_tmtc_backend(setup_args: SetupArgs, tm_handler: TmHandler): - """Creates a default TMTC backend instance which can be passed to the tmtccmd runner - - :param setup_args: - :param tm_handler: - :return: - """ - global __SETUP_WAS_CALLED - from tmtccmd.core.backend import TmTcHandler - from tmtccmd.sendreceive.tm_listener import TmListener - from typing import cast - - if not __SETUP_WAS_CALLED: - LOGGER.warning("setup_tmtccmd was not called first. Call it first") - sys.exit(1) - service, op_code, com_if_id, mode = __get_backend_init_variables() - if tm_handler is None: - LOGGER.warning( - "No TM Handler specified! Make sure to specify at least one TM handler" - ) - sys.exit(1) - else: - if tm_handler.get_type() == TmTypes.CCSDS_SPACE_PACKETS: - tm_handler = cast(CcsdsTmHandler, tm_handler) - apid = get_default_tc_apid() - com_if = setup_args.hook_obj.assign_communication_interface( - com_if_key=get_global(CoreGlobalIds.COM_IF) - ) - tm_timeout = get_global(CoreGlobalIds.TM_TIMEOUT) - tm_listener = TmListener(com_if=com_if, seq_timeout=tm_timeout) - # The global variables are set by the argument parser. - tmtc_backend = TmTcHandler( - com_if=com_if, - tm_listener=tm_listener, - init_mode=mode, - init_service=service, - init_opcode=op_code, - tm_handler=tm_handler, - ) - tmtc_backend.set_current_apid(apid=apid) - tmtc_backend.one_shot_operation = not get_global( - CoreGlobalIds.USE_LISTENER_AFTER_OP - ) - return tmtc_backend diff --git a/src/tmtccmd/sendreceive/__init__.py b/src/tmtccmd/sendreceive/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tmtccmd/sendreceive/cmd_sender_receiver.py b/src/tmtccmd/sendreceive/cmd_sender_receiver.py deleted file mode 100644 index fd45c0f8..00000000 --- a/src/tmtccmd/sendreceive/cmd_sender_receiver.py +++ /dev/null @@ -1,204 +0,0 @@ -"""Base class for sender/receiver objects -@author: R. Mueller -""" -import time -from typing import Optional, Tuple -from tmtccmd.com_if.com_interface_base import CommunicationInterface -from tmtccmd.config.definitions import QueueCommands, CoreGlobalIds, UsrSendCbT -from tmtccmd.logging import get_console_logger - -from tmtccmd.ccsds.handler import CcsdsTmHandler -from tmtccmd.sendreceive.tm_listener import TmListener -from tmtccmd.tc.definitions import TcQueueEntryT -from tmtccmd.core.globals_manager import get_global - -LOGGER = get_console_logger() - - -class CommandSenderReceiver: - """ - This is the generic CommandSenderReceiver object. All TMTC objects inherit this object, - for example specific implementations (e.g. SingleCommandSenderReceiver) - """ - - def __init__( - self, - com_if: CommunicationInterface, - tm_listener: TmListener, - tm_handler: CcsdsTmHandler, - apid: int, - usr_send_wrapper: Optional[Tuple[UsrSendCbT, any]] = None, - ): - - """ - :param com_if: CommunicationInterface object. Instantiate the desired one - and pass it here - """ - self._tm_timeout = get_global(CoreGlobalIds.TM_TIMEOUT) - self._tm_handler = tm_handler - self._tc_send_timeout_factor = get_global(CoreGlobalIds.TC_SEND_TIMEOUT_FACTOR) - self._apid = apid - self._usr_send_cb: Optional[UsrSendCbT] = None - self._usr_send_args: Optional[any] = None - if usr_send_wrapper is not None: - self._usr_send_cb = usr_send_wrapper[0] - self._usr_send_args = usr_send_wrapper[1] - - if isinstance(com_if, CommunicationInterface): - self._com_if = com_if - else: - LOGGER.error("CommandSenderReceiver: Invalid communication interface!") - raise TypeError("CommandSenderReceiver: Invalid communication interface!") - - if isinstance(tm_listener, TmListener): - self._tm_listener = tm_listener - else: - LOGGER.error("CommandSenderReceiver: Invalid TM listener!") - raise TypeError("Invalid TM Listener!") - - self._start_time = 0 - self._elapsed_time = 0 - self._timeout_counter = 0 - - # needed to store last actual TC packet from queue - self._last_tc = bytearray() - self._last_tc_obj = None - - # this flag can be used to notify when the operation is finished - self._operation_pending = False - - self._wait_period = 0 - self._wait_start = 0 - self._wait_end = 0 - - def set_tm_timeout(self, tm_timeout: float = -1): - """ - Set the TM timeout. Usually, the global value set by the args parser is set, - but the TM timeout can be reset (e.g. for slower architectures) - :param tm_timeout: New TM timeout value as a float value in seconds - :return: - """ - if tm_timeout == -1: - tm_timeout = get_global(CoreGlobalIds.TM_TIMEOUT) - self._tm_timeout = tm_timeout - - def set_tc_send_timeout_factor(self, new_factor: float = -1): - """ - Set the TC resend timeout factor. After self._tm_timeout * new_factor seconds, - a telecommand will be resent again. - :param new_factor: Factor as a float number - :return: - """ - if new_factor == -1: - new_factor = get_global(CoreGlobalIds.TC_SEND_TIMEOUT_FACTOR) - self._tc_send_timeout_factor = new_factor - - def _check_for_first_reply(self) -> bool: - """ - Checks for replies. If no reply is received, send telecommand again in checkForTimeout() - :return: None - """ - if self._tm_listener.reply_event(): - self._operation_pending = False - self._tm_listener.clear_reply_event() - return True - else: - return self._check_for_tm_timeout() - - def wait_period_ongoing( - self, - sleep_rest_of_wait_period: bool = False, - ) -> bool: - """If the first argument is set to true, this function will reset the internal wait time - variable to 0 - """ - if sleep_rest_of_wait_period: - # wait rest of wait time - sleep_time = self._wait_end - time.time() - if sleep_time > 0: - time.sleep(sleep_time) - LOGGER.info("Wait period over.") - return False - # If wait period was specified, we need to wait before checking the next queue entry. - if self._wait_period > 0: - if time.time() - self._wait_start < self._wait_period: - return True - else: - LOGGER.info("Wait period over.") - self._wait_period = 0 - return False - else: - return False - - @staticmethod - def check_queue_entry_static(tc_queue_entry: TcQueueEntryT) -> bool: - """Static method to check whether a queue entry is a valid telecommand""" - queue_entry_first, queue_entry_second = tc_queue_entry - if isinstance(queue_entry_first, str): - LOGGER.warning("Invalid telecommand. Queue entry is a string!") - return False - if isinstance(queue_entry_first, QueueCommands): - return False - elif isinstance(queue_entry_first, bytearray): - return True - else: - return False - - def check_queue_entry(self, tc_queue_entry: TcQueueEntryT) -> bool: - """ - Checks whether the entry in the pus_tc queue is a telecommand. - The last telecommand and respective information are stored in _last_tc - and _last_tc_info - :param tc_queue_entry: - :return: True if queue entry is telecommand, False if it is not - """ - queue_entry_first, queue_entry_second = tc_queue_entry - queue_entry_is_telecommand = False - - if isinstance(queue_entry_first, str): - LOGGER.warning("Invalid telecommand. Queue entry is a string!") - return queue_entry_is_telecommand - - if queue_entry_first == QueueCommands.WAIT: - wait_time = queue_entry_second - self._wait_period = wait_time - self._wait_start = time.time() - self._wait_end = self._wait_start + self._wait_period - LOGGER.info(f"Waiting for {self._wait_period} seconds.") - # printout optimized for LOGGER and debugging - elif queue_entry_first == QueueCommands.PRINT: - LOGGER.info(queue_entry_second) - elif queue_entry_first == QueueCommands.RAW_PRINT: - LOGGER.info(f"Raw command: {queue_entry_second.hex(sep=',')}") - elif queue_entry_first == QueueCommands.SET_TIMEOUT: - self._tm_timeout = queue_entry_second - self._tm_listener.seq_timeout = queue_entry_second - else: - self._last_tc, self._last_tc_obj = (queue_entry_first, queue_entry_second) - return True - return queue_entry_is_telecommand - - def _check_for_tm_timeout(self, resend_tc: bool = False) -> bool: - """ - Checks whether a timeout after sending a telecommand has occured and sends telecommand - again. If resending reached certain counter, exit the program. - :return: - """ - if self._start_time == 0: - raise True - if self._timeout_counter == 5: - LOGGER.info("CommandSenderReceiver: No response from command !") - self._operation_pending = False - self._elapsed_time = time.time() - self._start_time - if self._elapsed_time >= self._tm_timeout * self._tc_send_timeout_factor: - if resend_tc: - LOGGER.info("CommandSenderReceiver: Timeout, sending TC again !") - self._com_if.send(self._last_tc) - self._timeout_counter = self._timeout_counter + 1 - self._start_time = time.time() - return False - else: - # todo: we could also stop sending and clear the TC queue - return True - else: - return False diff --git a/src/tmtccmd/sendreceive/multiple_cmds_sender_receiver.py b/src/tmtccmd/sendreceive/multiple_cmds_sender_receiver.py deleted file mode 100644 index a2272b18..00000000 --- a/src/tmtccmd/sendreceive/multiple_cmds_sender_receiver.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Used to send multiple TCs as bursts and listen for replies simultaneously. Used by Module Tester -""" -import sys -import time -from typing import Union, Deque, Optional, Tuple -from collections import deque - -from tmtccmd.ccsds.handler import CcsdsTmHandler -from tmtccmd.config.definitions import UsrSendCbT -from tmtccmd.sendreceive.sequential_sender_receiver import ( - SequentialCommandSenderReceiver, -) -from tmtccmd.com_if.com_interface_base import CommunicationInterface -from tmtccmd.sendreceive.tm_listener import TmListener -from tmtccmd.utility.tmtc_printer import get_console_logger - - -LOGGER = get_console_logger() - - -class MultipleCommandSenderReceiver(SequentialCommandSenderReceiver): - """Difference to sequential sender: This class can send TCs in bursts. - Wait intervals can be specified with wait time between the send bursts. - """ - - def __init__( - self, - com_if: CommunicationInterface, - apid: int, - tm_handler: CcsdsTmHandler, - tm_listener: TmListener, - tc_queue: Deque, - wait_intervals: list, - wait_time: Union[float, list], - print_tm: bool, - usr_send_wrapper: Optional[Tuple[UsrSendCbT, any]] = None, - ): - """TCs are sent in burst when applicable. Wait intervals can be specified by supplying - respective arguments - - :param com_if: - :param tc_queue: - :param wait_intervals: List of pause intervals. For example [1,3] means that a wait_time - is applied after - sendinf the first and the third telecommand - :param wait_time: List of wait times or uniform wait time as float - :param print_tm: - """ - super().__init__( - com_if=com_if, - tm_listener=tm_listener, - tc_queue=tc_queue, - apid=apid, - tm_handler=tm_handler, - usr_send_wrapper=usr_send_wrapper, - ) - self.waitIntervals = wait_intervals - self.waitTime = wait_time - self.printTm = print_tm - self.tm_packet_queue = deque() - self.tc_info_queue = deque() - self.pusPacketInfo = [] - self.pusPacket = [] - self.waitCounter = 0 - - def send_tc_queue_and_return_info(self): - try: - self._tm_listener.manual_mode() - self._tm_listener.event_mode_change.set() - time.sleep(0.1) - # TC info queue is set in this function - self.__send_all_queue() - time.sleep(self._tm_timeout / 1.4) - # Get a copy of the queue, otherwise we will lose the data. - tm_packet_queue_list = ( - self._tm_listener.retrieve_ccsds_tm_packet_queue().copy() - ) - self._tm_listener.clear_ccsds_tm_packet_queue(apid=self._apid) - return self.tc_info_queue, tm_packet_queue_list - except (KeyboardInterrupt, SystemExit): - LOGGER.info("Keyboard Interrupt.") - sys.exit() - - def __handle_tc_resending(self): - while not self.__all_replies_received: - if self._tc_queue.__len__ == 0: - if self._start_time == 0: - self._start_time = time.time() - self._check_for_tm_timeout() - - def __send_all_queue(self): - while not self._tc_queue.__len__() == 0: - self._send_next_telecommand() - - def __handle_waiting(self): - self.waitCounter = self.waitCounter + 1 - if self.waitCounter in self.waitIntervals: - if isinstance(self.waitTime, list): - time.sleep(self.waitTime[self.waitIntervals.index(self.waitCounter)]) - else: - time.sleep(self.waitTime) - if self.waitTime == 0: - # To prevent thread starvation - time.sleep(0.1) - - def __retrieve_listener_tm_packet_queue(self): - if self._tm_listener.reply_event(): - return self._tm_listener.retrieve_ccsds_tm_packet_queue(apid=self._apid) - else: - LOGGER.error( - "Multiple Command SenderReceiver: Configuration error, " - "reply event not set in TM listener" - ) - - def __clear_listener_tm_info_queue(self): - self._tm_listener.clear_tm_packet_queues(True) diff --git a/src/tmtccmd/sendreceive/sequential_sender_receiver.py b/src/tmtccmd/sendreceive/sequential_sender_receiver.py deleted file mode 100644 index c273c542..00000000 --- a/src/tmtccmd/sendreceive/sequential_sender_receiver.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/python3.8 -""" -@file tmtcc_sequential_sender_receiver.py -@date 01.11.2019 -@brief Used to send multiple TCs in sequence and listen for replies after each sent TC -""" -import sys -import time -from typing import Optional, Tuple - -from tmtccmd.config.definitions import UsrSendCbT -from tmtccmd.sendreceive.cmd_sender_receiver import CommandSenderReceiver -from tmtccmd.ccsds.handler import CcsdsTmHandler -from tmtccmd.sendreceive.tm_listener import TmListener -from tmtccmd.com_if.com_interface_base import CommunicationInterface -from tmtccmd.logging import get_console_logger -from tmtccmd.tc.definitions import TcQueueT - -import threading - -LOGGER = get_console_logger() - - -class SequentialCommandSenderReceiver(CommandSenderReceiver): - """Specific implementation of CommandSenderReceiver to send multiple telecommands in sequence""" - - def __init__( - self, - com_if: CommunicationInterface, - tm_handler: CcsdsTmHandler, - apid: int, - tm_listener: TmListener, - tc_queue: TcQueueT, - usr_send_wrapper: Optional[Tuple[UsrSendCbT, any]] = None, - ): - """ - :param com_if: CommunicationInterface object, passed on to CommandSenderReceiver - :param tm_listener: TmListener object which runs in the background and receives - all Telemetry - """ - super().__init__( - com_if=com_if, - tm_listener=tm_listener, - tm_handler=tm_handler, - apid=apid, - usr_send_wrapper=usr_send_wrapper, - ) - self._tc_queue = tc_queue - self.__all_replies_received = False - # This flag can be used to notify the sender to send the next TC - self._next_send_condition = False - - # create a daemon (which will exit automatically if all other threads are closed) - # to handle telemetry - # this is an optional functionality which can be used by the TmTcHandler aka backend - self.daemon_thread = threading.Thread( - target=self.__perform_daemon_operation, daemon=True - ) - - def set_tc_queue(self, tc_queue: TcQueueT): - self._tc_queue = tc_queue - - def send_queue_tc_and_receive_tm_sequentially(self): - """Primary function which is called for sequential transfer. - :return: - """ - self._tm_listener.sequence_mode() - # tiny delay for pus_tm listener - time.sleep(0.05) - if self._tc_queue: - try: - self.__handle_tc_sending_and_tm_reception() - except (KeyboardInterrupt, SystemExit): - LOGGER.info("Keyboard Interrupt.") - sys.exit() - else: - LOGGER.warning("Supplied TC queue is empty!") - - def send_queue_tc_and_return(self): - self._tm_listener.listener_mode() - # tiny delay for pus_tm listener - time.sleep(0.05) - if self._tc_queue: - try: - # Set to true for first packet, otherwise nothing will be sent. - self._next_send_condition = True - if not self._tc_queue.__len__() == 0: - self.__check_next_tc_send() - except (KeyboardInterrupt, SystemExit): - LOGGER.info("Keyboard Interrupt.") - sys.exit() - else: - LOGGER.warning("Supplied TC queue is empty!") - - def start_daemon(self): - if not self.daemon_thread.is_alive(): - self.daemon_thread.start() - - def __perform_daemon_operation(self): - while True: - self.__check_for_reply() - time.sleep(0.2) - - def __print_rem_timeout(self, op_divider: int, divisor: int = 15): - if op_divider % divisor == 0: - rem_time = self._wait_end - time.time() - if rem_time > 0: - LOGGER.info(f"{rem_time:.01f} seconds wait time remaining") - - def __handle_tc_sending_and_tm_reception(self): - """Internal function which handles the given TC queue while also simultaneously - polling all TM. - TODO: Make it testable by not delaying here and removing the loop, make - this function runnable in discrete steps - """ - # Set to true for first packet, otherwise nothing will be sent. - self._next_send_condition = True - next_sleep = 0.2 - op_divider = 0 - tc_queue_is_empty_and_processed = False - while not self.__all_replies_received: - # Do not use continue anywhere in this while loop for now - if not tc_queue_is_empty_and_processed: - if self._tc_queue.__len__() == 0: - if self._wait_period == 0: - # cache this for last wait time - self._start_time = time.time() - tc_queue_is_empty_and_processed = True - self.__check_for_reply() - if not tc_queue_is_empty_and_processed: - if not self.wait_period_ongoing(): - self._wait_period = 0 - self.__check_next_tc_send() - self.__print_rem_timeout(op_divider=op_divider) - time.sleep(next_sleep) - else: - if not self._check_for_tm_timeout(): - self.__check_for_reply() - self.__print_rem_timeout(op_divider=op_divider) - # Delay for a bit longer in case we are waiting for the TM timeout - next_sleep = 0.5 - else: - self.__all_replies_received = True - break - time.sleep(next_sleep) - op_divider += 1 - self._tm_listener.set_mode_op_finished() - LOGGER.info("SequentialSenderReceiver: All replies received!") - - def __check_for_reply(self): - if self._tm_listener.reply_event(): - self._reply_received = True - self._tm_listener.clear_reply_event() - packet_queue = self._tm_listener.retrieve_ccsds_tm_packet_queue( - apid=self._apid, clear=True - ) - self._tm_handler.handle_ccsds_packet_queue( - apid=self._apid, tm_queue=packet_queue - ) - # This makes reply reception more responsive - elif self._tm_listener.tm_packets_available(): - packet_queue = self._tm_listener.retrieve_ccsds_tm_packet_queue( - apid=self._apid, clear=True - ) - self._tm_handler.handle_ccsds_packet_queue( - apid=self._apid, tm_queue=packet_queue - ) - - def __check_next_tc_send(self): - # this flag is set in the separate receiver thread too - if self._next_send_condition: - if self._send_next_telecommand(): - self._next_send_condition = False - # just calculate elapsed time if start time has already been set (= command has been sent) - else: - if self._check_for_tm_timeout(): - self._next_send_condition = True - - def _send_next_telecommand(self) -> bool: - """Sends the next telecommand and returns whether an actual telecommand was sent""" - # Queue empty. Can happen because a wait period might still be ongoing - if not self._tc_queue: - return False - if self.wait_period_ongoing(): - return False - tc_queue_tuple = self._tc_queue.pop() - if self.check_queue_entry(tc_queue_tuple): - self._start_time = time.time() - packet, cmd_info = tc_queue_tuple - if self._usr_send_cb is not None: - try: - self._usr_send_cb( - packet, self._com_if, cmd_info, self._usr_send_args - ) - except TypeError: - LOGGER.exception("User TC send callback invalid") - else: - self._com_if.send(packet) - return True - - # queue empty. - elif not self._tc_queue: - # Another special case: Last queue entry is to wait. - if self._wait_period > 0: - if self.wait_period_ongoing(): - return False - self._wait_period = 0 - self.__all_replies_received = True - return False - else: - if self._usr_send_cb is not None: - queue_cmd, queue_cmd_arg = tc_queue_tuple - try: - self._usr_send_cb( - queue_cmd, self._com_if, queue_cmd_arg, self._usr_send_args - ) - except TypeError: - LOGGER.exception("User TC send callback invalid") - # If the queue entry was not a telecommand, send next telecommand - self.__check_next_tc_send() - return True diff --git a/src/tmtccmd/sendreceive/single_command_sender_receiver.py b/src/tmtccmd/sendreceive/single_command_sender_receiver.py deleted file mode 100644 index 25f04647..00000000 --- a/src/tmtccmd/sendreceive/single_command_sender_receiver.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/python3.8 -""" -@file - tmtcc_config.py -@date - 01.11.2019 -@brief - Used to send single tcs and listen for replies after that -""" -from typing import Optional, Tuple - -from tmtccmd.ccsds.handler import CcsdsTmHandler -from tmtccmd.config.definitions import UsrSendCbT -from tmtccmd.sendreceive.cmd_sender_receiver import CommandSenderReceiver -from tmtccmd.sendreceive.tm_listener import TmListener -from tmtccmd.com_if.com_interface_base import CommunicationInterface - -from tmtccmd.logging import get_console_logger - -from tmtccmd.tc.definitions import PusTcTupleT - - -logger = get_console_logger() - - -class SingleCommandSenderReceiver(CommandSenderReceiver): - """ - Specific implementation of CommandSenderReceiver to send a single telecommand - This object can be used by instantiating it and calling sendSingleTcAndReceiveTm() - """ - - def __init__( - self, - com_if: CommunicationInterface, - tm_listener: TmListener, - tm_handler: CcsdsTmHandler, - apid: int, - usr_send_wrapper: Optional[Tuple[UsrSendCbT, any]] = None, - ): - """ - :param com_if: CommunicationInterface object, passed on to CommandSenderReceiver - :param tm_listener: TmListener object which runs in the background and receives all TM - """ - super().__init__( - com_if=com_if, - tm_listener=tm_listener, - tm_handler=tm_handler, - apid=apid, - usr_send_wrapper=usr_send_wrapper, - ) - - def send_single_tc_and_receive_tm(self, pus_packet_tuple: PusTcTupleT): - """ - Send a single telecommand passed to the class and wait for replies - :return: - """ - try: - tuple_first, tuple_second = pus_packet_tuple - except TypeError: - logger.error("SingleCommandSenderReceiver: Invalid command input") - return - self._operation_pending = True - self._tm_listener.sequence_mode() - if self._usr_send_cb is not None: - self._usr_send_cb( - tuple_first, self._com_if, tuple_first, self._usr_send_args - ) - else: - self._com_if.send(tuple_first) - # TODO: What if entry is not a telecommand? - self._last_tc = tuple_first - self._last_tc_obj = tuple_second - while self._operation_pending: - # wait until reply is received - super()._check_for_first_reply() - if self._next_send_condition: - self._tm_listener.set_mode_op_finished() - packet_queue = self._tm_listener.retrieve_ccsds_tm_packet_queue( - apid=self._apid, clear=True - ) - self._tm_handler.handle_ccsds_packet_queue( - apid=self._apid, tm_queue=packet_queue - ) - logger.info("SingleCommandSenderReceiver: Reply received") - logger.info("Listening for packages ...") diff --git a/src/tmtccmd/sendreceive/tm_listener.py b/src/tmtccmd/sendreceive/tm_listener.py deleted file mode 100644 index 2e924491..00000000 --- a/src/tmtccmd/sendreceive/tm_listener.py +++ /dev/null @@ -1,396 +0,0 @@ -""" -@file tmtcc_tm_listener.py -@date 01.11.2019 -@brief Separate class to listen to telecommands. -@author R. Mueller -""" -import sys -import time -import threading -from collections import deque -from typing import Dict, List, Tuple, Optional -from enum import Enum - -from spacepackets.ccsds.spacepacket import get_apid_from_raw_space_packet - -from tmtccmd.tm.definitions import TelemetryQueueT, TelemetryListT, TmTypes -from tmtccmd.logging import get_console_logger -from tmtccmd.com_if.com_interface_base import CommunicationInterface -from tmtccmd.utility.conf_util import acquire_timeout - -LOGGER = get_console_logger() - -INVALID_APID = -2 -UNKNOWN_TARGET_ID = -1 -QueueDictT = Dict[int, Tuple[TelemetryQueueT, int]] -QueueListT = List[Tuple[int, TelemetryQueueT]] - - -class ListenerModes(Enum): - MANUAL = 1 - LISTENER = 2 - SEQUENCE = 3 - - -class TmListener: - """Performs all TM listening operations. - This listener to have a permanent means to receive data. A background thread is used - to poll data with the provided communication interface. Dedicated sender and receiver object - or any other software component can get the received packets from the internal deque container. - """ - - DEFAULT_MODE_OPERATION_TIMEOUT = 300.0 - DEFAULT_UNKNOWN_QUEUE_MAX_LEN = 50 - QUEUE_DICT_QUEUE_IDX = 0 - QUEUE_DICT_MAX_LEN_IDX = 1 - DEFAULT_TM_TIMEOUT = 5.0 - - DEFAULT_LOCK_TIMEOUT = 0.5 - - def __init__( - self, - com_if: CommunicationInterface, - seq_timeout: float = DEFAULT_TM_TIMEOUT, - tm_type: TmTypes = TmTypes.CCSDS_SPACE_PACKETS, - ): - """Initiate a TM listener. - :param com_if: Type of communication interface, - e.g. a serial or ethernet interface - :param tm_type: Telemetry type. Default to CCSDS space packets for now - """ - self.__com_if = com_if - self._mode_op_timeout = TmListener.DEFAULT_MODE_OPERATION_TIMEOUT - # TM Listener operations can be suspended by setting this flag - self.event_listener_active = threading.Event() - self.listener_active = False - self.current_apid = INVALID_APID - - # Listener is daemon and will exit automatically if all other threads are closed - self.listener_thread = threading.Thread( - target=self.__perform_operation, daemon=True - ) - self.lock_listener = threading.Lock() - # This Event is set by sender objects to perform mode operations - self.event_mode_change = threading.Event() - # This Event is set and cleared by the listener to inform the sender objects - # if a reply has been received - self.__event_reply_received = threading.Event() - # This Event is set by sender objects if all necessary operations are done - # to transition back to listener mode - self.__event_mode_op_finished = threading.Event() - - self.__listener_mode = ListenerModes.LISTENER - self.seq_timeout = seq_timeout - self.__tm_type = tm_type - self.__queue_dict: QueueDictT = dict( - {UNKNOWN_TARGET_ID: (deque(), self.DEFAULT_UNKNOWN_QUEUE_MAX_LEN)} - ) - - def start(self): - if not self.event_listener_active.is_set(): - self.event_listener_active.set() - if not self.listener_thread.is_alive(): - self.listener_thread.start() - else: - LOGGER.warning("TM listener is already active!") - - def stop(self): - self.event_listener_active.clear() - - def set_mode_op_timeout(self, timeout: float): - self._mode_op_timeout = timeout - - def subscribe_ccsds_tm_handler(self, apid: int, queue_max_len: int): - if self.__tm_type == TmTypes.CCSDS_SPACE_PACKETS: - self.__queue_dict[apid] = (deque(), queue_max_len) - else: - LOGGER.warning("This function only support CCSDS space packet handling") - - def set_current_apid(self, new_apid: int): - self.current_apid = new_apid - - def set_com_if(self, com_if: CommunicationInterface): - self.__com_if = com_if - - def is_listener_active(self) -> bool: - return self.listener_active - - def manual_mode(self) -> bool: - return self.__update_mode(ListenerModes.MANUAL) - - def listener_mode(self) -> bool: - return self.__update_mode(ListenerModes.LISTENER) - - def sequence_mode(self, seq_timeout: Optional[float] = None) -> bool: - if seq_timeout is not None: - self.seq_timeout = seq_timeout - return self.__update_mode(ListenerModes.SEQUENCE) - - def __update_mode(self, new_mode: ListenerModes) -> bool: - if self.__listener_mode != new_mode: - self.event_mode_change.set() - self.__listener_mode = new_mode - return True - return False - - def reply_event(self): - if self.__event_reply_received.is_set(): - return True - else: - return False - - def clear_reply_event(self): - self.__event_reply_received.clear() - - def set_mode_op_finished(self): - if not self.__event_mode_op_finished.is_set(): - self.__event_mode_op_finished.set() - - def ccsds_tm_received(self, apid: int = INVALID_APID): - """This function is used to check whether any data has been received""" - queue_dict_list = self.__queue_dict.get(apid) - if queue_dict_list is None: - LOGGER.warning(f"No queue available for APID {apid}") - queue_dict = queue_dict_list[self.QUEUE_DICT_QUEUE_IDX] - if queue_dict.__len__() > 0: - return True - else: - return False - - def tm_packets_available(self): - with acquire_timeout( - self.lock_listener, timeout=self.DEFAULT_LOCK_TIMEOUT - ) as acquired: - if acquired: - for queue_lists in self.__queue_dict.values(): - if queue_lists[self.QUEUE_DICT_QUEUE_IDX]: - return True - return False - - def retrieve_tm_packet_queues(self, clear: bool) -> QueueListT: - queues = [] - with acquire_timeout( - self.lock_listener, timeout=self.DEFAULT_LOCK_TIMEOUT - ) as acquired: - if not acquired: - LOGGER.error("Could not acquire lock!") - # Still continue - for key, queue_list in self.__queue_dict.items(): - queues.append((key, queue_list[self.QUEUE_DICT_QUEUE_IDX].copy())) - if clear: - self.clear_tm_packet_queues(lock=False) - return queues - - def retrieve_ccsds_tm_packet_queue( - self, apid: int = -1, clear: bool = False - ) -> TelemetryQueueT: - """Retrieve the packet queue for a given APID. The TM listener will handle routing - packets into the correct queue.""" - if apid == -1: - apid = self.current_apid - target_queue_list = self.__queue_dict.get(apid) - if target_queue_list is None: - LOGGER.warning(f"No queue available for APID {apid}") - return deque() - target_queue = target_queue_list[self.QUEUE_DICT_QUEUE_IDX] - # We make sure that the queue is not manipulated while it is being copied. - with acquire_timeout( - self.lock_listener, timeout=self.DEFAULT_LOCK_TIMEOUT - ) as acquired: - if not acquired: - LOGGER.warning( - f"TmListener: Blocked on lock acquisition for longer than" - f"{self.DEFAULT_LOCK_TIMEOUT} second!" - ) - tm_queue_copy = target_queue.copy() - if clear: - target_queue.clear() - return tm_queue_copy - - def clear_ccsds_tm_packet_queue(self, apid: int): - if apid == -1: - apid = self.current_apid - target_queue = self.__queue_dict.get(apid) - if target_queue is None: - LOGGER.warning(f"No queue available for APID {apid}") - return - with acquire_timeout( - self.lock_listener, timeout=self.DEFAULT_LOCK_TIMEOUT - ) as acquired: - if not acquired: - LOGGER.warning( - f"TmListener: Blocked on lock acquisition for longer than" - f"{self.DEFAULT_LOCK_TIMEOUT} second!" - ) - target_queue[0].clear() - - def clear_tm_packet_queues(self, lock: bool): - locked = False - if lock: - locked = self.lock_listener.acquire(timeout=self.DEFAULT_LOCK_TIMEOUT) - for queue_list in self.__queue_dict.values(): - queue_list[self.QUEUE_DICT_QUEUE_IDX].clear() - if locked: - self.lock_listener.release() - - def retrieve_unknown_target_queue(self): - target_queue = self.__queue_dict.get(UNKNOWN_TARGET_ID)[ - self.QUEUE_DICT_QUEUE_IDX - ] - with acquire_timeout( - self.lock_listener, timeout=self.DEFAULT_LOCK_TIMEOUT - ) as acquired: - if acquired: - return target_queue.copy() - - def check_for_one_telemetry_sequence(self, seq_timeout: float) -> bool: - """Receive all telemetry for a specified time period. - :return: True if a sequence was received - """ - data_available = self.__com_if.data_available(timeout=0, parameters=None) - if data_available == 0: - return False - elif data_available > 0: - self.__read_telemetry_sequence(tm_timeout=seq_timeout) - return True - else: - LOGGER.error("TmListener: Configuration error in communication interface!") - sys.exit() - - def __perform_operation(self): - while True: - # This is running in a daemon thread so it will stop automatically if all other - # threads have closed - if self.event_listener_active.is_set(): - self.listener_active = True - self.__default_operation() - else: - self.listener_active = False - # Check every 300 ms whether connection is up again. - time.sleep(0.3) - - def __default_operation(self): - """Core function. Normally, polls all packets""" - self.__perform_core_operation() - if self.event_mode_change.is_set(): - self.event_mode_change.clear() - start_time = time.time() - while not self.__event_mode_op_finished.is_set(): - elapsed_time = time.time() - start_time - if elapsed_time < self._mode_op_timeout: - self.__perform_mode_operation() - else: - LOGGER.warning("TmListener: Mode operation timeout occured!") - break - self.__event_mode_op_finished.clear() - LOGGER.info("TmListener: Transitioning to listener mode.") - self.__listener_mode = ListenerModes.LISTENER - - def __perform_core_operation(self): - """The core operation listens for packets.""" - packet_list = self.__com_if.receive() - if len(packet_list) > 0: - with acquire_timeout( - self.lock_listener, timeout=self.DEFAULT_LOCK_TIMEOUT - ) as acquired: - if not acquired: - LOGGER.warning( - f"TmListener: Blocked on lock acquisition for longer than" - f"{self.DEFAULT_LOCK_TIMEOUT} second!" - ) - self.__route_packets(packet_list) - if not self.__event_reply_received.is_set(): - self.__event_reply_received.set() - else: - time.sleep(0.4) - - def __perform_mode_operation(self): - """The TmListener is instructed performs certain operations based on the current - listener mode. - :return: - """ - # Listener Mode - if self.__listener_mode == ListenerModes.LISTENER: - if not self.__event_mode_op_finished.is_set(): - self.__event_mode_op_finished.set() - # Single Command Mode - elif self.__listener_mode == ListenerModes.SEQUENCE: - # This prevents the listener from listening from one more unnecessary cycle - if self.__event_mode_op_finished.is_set(): - return - # Listen for one reply sequence. - if self.check_for_one_telemetry_sequence(self.seq_timeout): - # Set reply event, will be cleared by checkForFirstReply() - if not self.__event_reply_received.is_set(): - self.__event_reply_received.set() - time.sleep(0.2) - elif self.__listener_mode == ListenerModes.MANUAL: - self.__perform_core_operation() - - def __read_telemetry_sequence(self, tm_timeout: float): - """Thread-safe implementation for reading a telemetry sequence.""" - start_time = time.time() - elapsed_time = 0 - while elapsed_time < tm_timeout: - # Fast responsiveness in sequential mode - if self.__event_mode_op_finished.is_set(): - if self.__listener_mode == ListenerModes.SEQUENCE: - return - packets_available = self.__com_if.data_available( - timeout=0.2, parameters=None - ) - if packets_available > 0: - packet_list = self.__com_if.receive() - with acquire_timeout( - self.lock_listener, timeout=self.DEFAULT_LOCK_TIMEOUT - ) as acquired: - if not acquired: - LOGGER.warning( - f"TmListener: Blocked on lock acquisition for longer than" - f"{self.DEFAULT_LOCK_TIMEOUT} second!" - ) - self.__route_packets(packet_list) - elapsed_time = time.time() - start_time - if packets_available == 0: - time.sleep(0.1) - - def __route_packets(self, tm_packet_list: TelemetryListT): - """Route given packets. For CCSDS packets, use APID to do this""" - for tm_packet in tm_packet_list: - if self.__tm_type == TmTypes.CCSDS_SPACE_PACKETS: - packet_handled = self.__handle_ccsds_space_packet(tm_packet=tm_packet) - if packet_handled: - continue - # No queue was found - LOGGER.warning("No target queue found, inserting into unknown target queue") - unknown_target_list = self.__queue_dict[UNKNOWN_TARGET_ID] - unknown_target_queue = unknown_target_list[self.QUEUE_DICT_QUEUE_IDX] - if ( - unknown_target_queue.__len__() - > unknown_target_list[self.QUEUE_DICT_MAX_LEN_IDX] - ): - LOGGER.warning("Unknown target queue full. Removing oldest packet..") - unknown_target_queue.pop() - unknown_target_queue.appendleft(tm_packet) - - def __handle_ccsds_space_packet(self, tm_packet: bytes) -> bool: - if len(tm_packet) < 6: - LOGGER.warning("TM packet to small to be a CCSDS space packet") - else: - apid = get_apid_from_raw_space_packet(raw_packet=tm_packet) - target_queue_list = self.__queue_dict.get(apid) - if target_queue_list is None: - LOGGER.warning(f"No TM handler assigned for APID {apid}") - else: - target_queue = target_queue_list[self.QUEUE_DICT_QUEUE_IDX] - if ( - target_queue.__len__() - > target_queue_list[self.QUEUE_DICT_MAX_LEN_IDX] - ): - LOGGER.warning( - f"Target queue for APID {apid} full. Removing oldest packet.." - ) - target_queue.pop() - target_queue.appendleft(tm_packet) - return True - return False diff --git a/src/tmtccmd/tc/__init__.py b/src/tmtccmd/tc/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/tmtccmd/tc/definitions.py b/src/tmtccmd/tc/definitions.py deleted file mode 100644 index 6652a6c0..00000000 --- a/src/tmtccmd/tc/definitions.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import Tuple, Union, Deque -from tmtccmd.config.definitions import QueueCommands -from spacepackets.ecss.tc import PusTelecommand - -TcAuxiliaryTupleT = Tuple[QueueCommands, any] -PusTcTupleT = Tuple[bytearray, Union[None, PusTelecommand]] -TcQueueEntryT = Union[TcAuxiliaryTupleT, PusTcTupleT] -TcQueueT = Deque[TcQueueEntryT] diff --git a/src/tmtccmd/tc/packer.py b/src/tmtccmd/tc/packer.py deleted file mode 100644 index 895281cc..00000000 --- a/src/tmtccmd/tc/packer.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -""" -:file: obsw_tc_packer.py -:author: R. Mueller -:date: 10.05.2021 -""" -import sys -from typing import Union - -from tmtccmd.tc.definitions import TcQueueT -from spacepackets.ecss.tc import PusTelecommand -from tmtccmd.logging import get_console_logger -from tmtccmd.pus.pus_17_test import pack_service_17_ping_command -from tmtccmd.tc.pus_5_event import pack_generic_service5_test_into - -LOGGER = get_console_logger() - - -class ServiceQueuePacker: - def __init__(self): - pass - - @staticmethod - def pack_service_queue_core(service: int, op_code: str, service_queue: TcQueueT): - """ - Use hook object supplied by user - """ - try: - from tmtccmd.config.hook import get_global_hook_obj - - hook_obj = get_global_hook_obj() - hook_obj.pack_service_queue( - service=service, op_code=op_code, service_queue=service_queue - ) - except ImportError: - LOGGER.exception( - "Could not import custom telecommand hook! Make sure to implement it." - ) - sys.exit(1) - - -def default_single_packet_preparation() -> PusTelecommand: - return pack_service_17_ping_command(ssc=1700) - - -def default_service_queue_preparation( - service: Union[str, int], op_code: str, service_queue: TcQueueT -): - from tmtccmd.config.definitions import CoreServiceList, QueueCommands - - if service == CoreServiceList.SERVICE_5.value: - pack_generic_service5_test_into(service_queue) - elif service == CoreServiceList.SERVICE_17.value: - service_queue.appendleft( - (QueueCommands.PRINT, "Sending ping command PUS TC[17,1]") - ) - service_queue.appendleft( - pack_service_17_ping_command(ssc=1700).pack_command_tuple() - ) - else: - LOGGER.warning("Invalid Service!") diff --git a/src/tmtccmd/tc/pus_5_event.py b/src/tmtccmd/tc/pus_5_event.py deleted file mode 100644 index 1650603b..00000000 --- a/src/tmtccmd/tc/pus_5_event.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Contains definitions and functions related to PUS Service 5 Telecommands. -""" -from spacepackets.ecss.conf import get_default_tc_apid -from spacepackets.ecss.pus_5_event import Subservices - -from tmtccmd.config.definitions import QueueCommands -from tmtccmd.tc.definitions import PusTelecommand, TcQueueT - - -def pack_enable_event_reporting_command(ssc: int, apid: int = -1): - if apid == -1: - apid = get_default_tc_apid() - return PusTelecommand( - service=5, subservice=Subservices.TC_ENABLE_EVENT_REPORTING, ssc=ssc, apid=apid - ) - - -def pack_disable_event_reporting_command(ssc: int, apid: int = -1): - if apid == -1: - apid = get_default_tc_apid() - return PusTelecommand( - service=5, - subservice=Subservices.TC_DISABLE_EVENT_REPORTING, - ssc=ssc, - apid=apid, - ) - - -def pack_generic_service5_test_into(tc_queue: TcQueueT, apid: int = -1): - if apid == -1: - apid = get_default_tc_apid() - tc_queue.appendleft((QueueCommands.PRINT, "Testing Service 5")) - # invalid subservice - tc_queue.appendleft((QueueCommands.PRINT, "Testing Service 5: Invalid subservice")) - command = PusTelecommand(service=5, subservice=1, apid=apid, ssc=500) - tc_queue.appendleft(command.pack_command_tuple()) - # disable events - tc_queue.appendleft((QueueCommands.PRINT, "Testing Service 5: Disable event")) - command = pack_disable_event_reporting_command(ssc=501) - tc_queue.appendleft(command.pack_command_tuple()) - # trigger event - tc_queue.appendleft((QueueCommands.PRINT, "Testing Service 5: Trigger event")) - command = PusTelecommand(service=17, subservice=128, apid=apid, ssc=510) - tc_queue.appendleft(command.pack_command_tuple()) - # enable event - tc_queue.appendleft((QueueCommands.PRINT, "Testing Service 5: Enable event")) - command = pack_enable_event_reporting_command(ssc=520) - tc_queue.appendleft(command.pack_command_tuple()) - # trigger event - tc_queue.appendleft( - (QueueCommands.PRINT, "Testing Service 5: Trigger another event") - ) - command = PusTelecommand(service=17, subservice=128, apid=apid, ssc=530) - tc_queue.appendleft(command.pack_command_tuple()) diff --git a/src/tmtccmd/tm/__init__.py b/src/tmtccmd/tm/__init__.py deleted file mode 100644 index 3cb80158..00000000 --- a/src/tmtccmd/tm/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from tmtccmd.tm.definitions import TelemetryListT, TelemetryQueueT -from tmtccmd.tm.pus_5_event import Service5Tm -from tmtccmd.tm.pus_8_funccmd import Service8FsfwTm -from tmtccmd.tm.pus_3_fsfw_hk import Service3FsfwTm -from tmtccmd.tm.pus_20_fsfw_parameters import Service20FsfwTm -from tmtccmd.tm.pus_200_fsfw_modes import Service200FsfwTm diff --git a/src/tmtccmd/tm/definitions.py b/src/tmtccmd/tm/definitions.py deleted file mode 100644 index 7d3c41ce..00000000 --- a/src/tmtccmd/tm/definitions.py +++ /dev/null @@ -1,24 +0,0 @@ -import enum -from typing import Deque, Tuple, List, Union -from spacepackets.ecss.tm import PusTelemetry -from tmtccmd.tm.base import PusTmInfoInterface, PusTmInterface - -TelemetryListT = List[bytes] -TelemetryQueueT = Deque[bytes] - -PusTmQueue = Deque[PusTelemetry] -PusTmTupleT = Tuple[bytes, PusTelemetry] - -PusTmListT = List[PusTelemetry] -PusTmQueueT = Deque[PusTmListT] -PusIFListT = List[Union[PusTmInfoInterface, PusTmInterface]] -PusIFQueueT = Deque[PusIFListT] - -PusTmListT = List[PusTelemetry] -PusTmObjQeue = Deque[PusTelemetry] -PusTmTupleQueueT = Deque[PusTmTupleT] - - -class TmTypes(enum.Enum): - NONE = enum.auto - CCSDS_SPACE_PACKETS = enum.auto diff --git a/src/tmtccmd/tm/handler.py b/src/tmtccmd/tm/handler.py deleted file mode 100644 index 44829f2b..00000000 --- a/src/tmtccmd/tm/handler.py +++ /dev/null @@ -1,9 +0,0 @@ -from tmtccmd.tm.definitions import TmTypes - - -class TmHandler: - def __init__(self, tm_type: TmTypes): - self._tm_type = tm_type - - def get_type(self): - return self._tm_type diff --git a/examples/config/__init__.py b/tests/__init__.py similarity index 100% rename from examples/config/__init__.py rename to tests/__init__.py diff --git a/tests/hook_obj_mock.py b/tests/hook_obj_mock.py new file mode 100644 index 00000000..85f5c67e --- /dev/null +++ b/tests/hook_obj_mock.py @@ -0,0 +1,92 @@ +from abc import abstractmethod +from typing import Optional +from unittest.mock import MagicMock +import argparse + +from tmtccmd.com_if import ComInterface +from tmtccmd.config import CoreModeList +from tmtccmd.config.tmtc import TmTcDefWrapper +from tmtccmd.core.ccsds_backend import CcsdsTmtcBackend +from tmtccmd.config import TmTcCfgHookBase +from tmtccmd.logging import get_console_logger +from tmtccmd.utility.obj_id import ObjectIdDictT + +LOGGER = get_console_logger() + + +def create_hook_mock() -> TmTcCfgHookBase: + """Create simple minimal hook mock using the MagicMock facilities by unittest + :return: + """ + tmtc_hook_base = TmTcCfgHookBase() + tmtc_hook_base.add_globals_pre_args_parsing = MagicMock(return_value=0) + tmtc_hook_base.add_globals_post_args_parsing = MagicMock(return_value=0) + tmtc_hook_base.custom_args_parsing = MagicMock( + return_value=argparse.Namespace(service=17, mode=CoreModeList.IDLE) + ) + return tmtc_hook_base + + +def create_hook_mock_with_srv_handlers() -> TmTcCfgHookBase: + tmtc_hook_base = create_hook_mock() + tmtc_hook_base.handle_service_8_telemetry = MagicMock(return_value=(["Test"], [0])) + # Valid returnvalue for now + srv_3_return_tuple = (["Test"], [0], bytearray(0b10000000), 1) + tmtc_hook_base.handle_service_3_housekeeping = MagicMock( + return_value=srv_3_return_tuple + ) + return tmtc_hook_base + + +class TestHookObj(TmTcCfgHookBase): + service_8_handler_called = False + service_5_handler_called = False + service_3_handler_called = False + + def __init__(self): + super().__init__() + self.get_obj_id_called = False + self.add_globals_pre_args_parsing_called = False + self.add_globals_post_args_parsing_called = False + self.assign_communication_interface_called = False + + @abstractmethod + def get_object_ids(self) -> ObjectIdDictT: + """The user can specify an object ID dictionary here mapping object ID bytearrays to a + list. This list could contain containing the string representation or additional + information about that object ID. + """ + return super().get_object_ids() + + @abstractmethod + def assign_communication_interface(self, com_if_key: str) -> Optional[ComInterface]: + """Assign the communication interface used by the TMTC commander to send and receive + TMTC with. + + :param com_if_key: String key of the communication interface to be created. + """ + from tmtccmd.config.com_if import create_communication_interface_default + + return create_communication_interface_default( + com_if_key=com_if_key, json_cfg_path=self.json_cfg_path + ) + + @abstractmethod + def get_tmtc_definitions(self) -> TmTcDefWrapper: + """This is a dicitonary mapping services represented by strings to an operation code + dictionary. + + :return: + """ + from tmtccmd.config.globals import get_default_tmtc_defs + + return get_default_tmtc_defs() + + @abstractmethod + def perform_mode_operation(self, tmtc_backend: CcsdsTmtcBackend, mode: int): + """Perform custom mode operations + :param tmtc_backend: + :param mode: + :return: + """ + pass diff --git a/tests/test_backend.py b/tests/test_backend.py new file mode 100644 index 00000000..6d01f6e8 --- /dev/null +++ b/tests/test_backend.py @@ -0,0 +1,199 @@ +from datetime import timedelta +from typing import Optional +from unittest import TestCase +from unittest.mock import MagicMock + +from spacepackets.ecss import PusTelecommand +from tmtccmd import CcsdsTmtcBackend, CcsdsTmListener, TcHandlerBase +from tmtccmd.com_if import ComInterface +from tmtccmd.com_if.dummy import DummyComIF +from tmtccmd.core import TcMode, TmMode, BackendRequest +from tmtccmd.core.ccsds_backend import NoValidProcedureSet +from tmtccmd.tc import ( + TcProcedureBase, + DefaultProcedureInfo, + TcProcedureType, + ProcedureHelper, + TcQueueEntryBase, +) +from tmtccmd.tc.handler import FeedWrapper + + +class TcHandlerMock(TcHandlerBase): + def __init__(self): + super().__init__() + self.is_feed_cb_valid = False + self.feed_cb_call_count = 0 + self.feed_cb_def_proc_count = 0 + self.send_cb_call_count = 0 + self.send_cb_call_args = None + self.send_cb_service_arg: Optional[str] = None + self.send_cb_op_code_arg: Optional[str] = None + + def send_cb(self, entry_helper: ProcedureHelper, com_if: ComInterface): + self.send_cb_call_count += 1 + self.send_cb_call_args = (entry_helper, com_if) + + def queue_finished_cb(self, info: TcProcedureBase): + pass + + def feed_cb(self, info: ProcedureHelper, wrapper: FeedWrapper): + self.feed_cb_call_count += 1 + self.send_cb_service_arg = None + self.send_cb_op_code_arg = None + if info is not None: + if info.proc_type == TcProcedureType.DEFAULT: + self.feed_cb_def_proc_count += 1 + def_info = info.to_def_procedure() + if def_info.service != "17": + self.is_feed_cb_valid = False + self.send_cb_service_arg = def_info.service + self.send_cb_op_code_arg = def_info.op_code + if def_info.service == "17": + if def_info.op_code == "0": + wrapper.queue_helper.add_pus_tc( + PusTelecommand(service=17, subservice=1) + ) + elif def_info.op_code == "1": + wrapper.queue_helper.add_pus_tc( + PusTelecommand(service=17, subservice=1) + ) + wrapper.queue_helper.add_pus_tc( + PusTelecommand(service=5, subservice=1) + ) + elif def_info.op_code == "2": + wrapper.queue_helper.add_pus_tc( + PusTelecommand(service=17, subservice=1) + ) + wrapper.queue_helper.add_wait(timedelta(milliseconds=20)) + wrapper.queue_helper.add_pus_tc( + PusTelecommand(service=5, subservice=1) + ) + + +class TestBackend(TestCase): + def setUp(self) -> None: + self.com_if = DummyComIF() + self.tm_listener = MagicMock(specs=CcsdsTmListener) + self.tc_handler = TcHandlerMock() + self.backend = CcsdsTmtcBackend( + tc_mode=TcMode.IDLE, + tm_mode=TmMode.IDLE, + com_if=self.com_if, + tm_listener=self.tm_listener, + tc_handler=self.tc_handler, + ) + self.assertEqual(self.backend.tm_listener, self.tm_listener) + + def test_idle(self): + self.assertEqual(self.backend.tm_mode, TmMode.IDLE) + self.assertEqual(self.backend.tc_mode, TcMode.IDLE) + self.assertEqual(self.backend.com_if.get_id(), "dummy") + self.assertEqual(self.backend.com_if_id, "dummy") + self.assertEqual(self.backend.inter_cmd_delay, timedelta()) + state = self.backend.state + self.assertEqual(state.tc_mode, TcMode.IDLE) + self.assertEqual(state.tm_mode, TmMode.IDLE) + self.assertEqual(state.next_delay, timedelta()) + self.assertEqual(state.request, BackendRequest.NONE) + self.backend.mode_to_req() + self.assertEqual(state.request, BackendRequest.DELAY_IDLE) + self.assertEqual(self.backend.inter_cmd_delay, timedelta()) + self.assertFalse(self.backend.com_if_active()) + self.assertFalse(self.com_if.is_open()) + + def test_basic_ops(self): + self.backend.start() + self.assertTrue(self.com_if.is_open()) + self.assertTrue(self.backend.com_if_active()) + + res = self.backend.periodic_op() + self.assertEqual(res.request, BackendRequest.DELAY_IDLE) + self.backend.tm_mode = TmMode.LISTENER + self.assertEqual(self.backend.tm_mode, TmMode.LISTENER) + res = self.backend.periodic_op() + self.tm_listener.operation.assert_called_once() + self.backend.poll_tm() + self.assertEqual(self.tm_listener.operation.call_count, 2) + self.assertEqual(res.request, BackendRequest.DELAY_LISTENER) + self.backend.tm_mode = TmMode.IDLE + self.backend.tc_mode = TcMode.ONE_QUEUE + with self.assertRaises(NoValidProcedureSet): + self.backend.periodic_op() + self.backend.current_procedure = DefaultProcedureInfo(service="17", op_code="0") + + res = self.backend.periodic_op() + # Only one queue entry which is handled immediately + self.assertEqual(res.request, BackendRequest.TERMINATION_NO_ERROR) + self.assertEqual(self.tc_handler.feed_cb_def_proc_count, 1) + self.assertEqual(self.tc_handler.feed_cb_call_count, 1) + self.assertEqual(self.tc_handler.send_cb_call_count, 1) + self.assertIsNotNone(self.tc_handler.send_cb_call_args) + self.assertIsNotNone(self.tc_handler.send_cb_call_args[0]) + self.assertIsNotNone(self.tc_handler.send_cb_call_args[1]) + self.assertEqual(self.tc_handler.send_cb_call_args[1], self.com_if) + cast_wrapper = self.tc_handler.send_cb_call_args[0] + pus_entry = cast_wrapper.to_pus_tc_entry() + self.assertEqual(pus_entry.pus_tc, PusTelecommand(service=17, subservice=1)) + self.backend.close_com_if() + self.assertFalse(self.com_if.is_open()) + + def test_one_queue_multi_entry_ops(self): + self.backend.tm_mode = TmMode.IDLE + self.backend.tc_mode = TcMode.ONE_QUEUE + self.backend.current_procedure = DefaultProcedureInfo(service="17", op_code="1") + res = self.backend.periodic_op() + self.assertEqual(res.request, BackendRequest.CALL_NEXT) + self.assertEqual(self.tc_handler.feed_cb_def_proc_count, 1) + self.assertEqual(self.tc_handler.feed_cb_call_count, 1) + self.assertEqual(self.tc_handler.send_cb_call_count, 1) + self._check_tc_req_recvd(17, 1) + res = self.backend.periodic_op() + self.assertEqual(self.tc_handler.feed_cb_def_proc_count, 1) + self.assertEqual(self.tc_handler.feed_cb_call_count, 1) + self.assertEqual(self.tc_handler.send_cb_call_count, 2) + self._check_tc_req_recvd(5, 1) + self.assertEqual(res.request, BackendRequest.TERMINATION_NO_ERROR) + + def test_multi_queue_ops(self): + self.backend.tm_mode = TmMode.IDLE + self.backend.tc_mode = TcMode.MULTI_QUEUE + self.backend.current_procedure = DefaultProcedureInfo(service="17", op_code="0") + res = self.backend.periodic_op() + self.assertEqual(res.request, BackendRequest.CALL_NEXT) + self.assertEqual(self.backend.request, BackendRequest.CALL_NEXT) + self.assertEqual(self.backend.tc_mode, TcMode.IDLE) + self.assertEqual(self.tc_handler.feed_cb_def_proc_count, 1) + self.assertEqual(self.tc_handler.feed_cb_call_count, 1) + self.assertEqual(self.tc_handler.send_cb_call_count, 1) + self._check_tc_req_recvd(17, 1) + res = self.backend.periodic_op() + self.assertEqual(self.tc_handler.feed_cb_call_count, 1) + self.assertEqual(res.request, BackendRequest.DELAY_IDLE) + self.backend.tc_mode = TcMode.MULTI_QUEUE + self.backend.current_procedure = DefaultProcedureInfo(service="17", op_code="0") + res = self.backend.periodic_op() + self.assertEqual(res.request, BackendRequest.CALL_NEXT) + self.assertEqual(self.backend.request, BackendRequest.CALL_NEXT) + self.assertEqual(self.tc_handler.feed_cb_def_proc_count, 2) + self.assertEqual(self.tc_handler.feed_cb_call_count, 2) + + def test_procedure_handling(self): + def_proc = DefaultProcedureInfo(service="17", op_code="0") + self.backend.current_procedure = def_proc + self.assertEqual( + self.backend.current_procedure.proc_type, TcProcedureType.DEFAULT + ) + proc_helper = self.backend.current_procedure + def_proc = proc_helper.to_def_procedure() + self.assertIsNotNone(def_proc) + self.assertEqual(def_proc.service, "17") + self.assertEqual(def_proc.op_code, "0") + + def _check_tc_req_recvd(self, service: int, subservice: int): + self.assertEqual(self.tc_handler.send_cb_call_args[1], self.com_if) + cast_wrapper = self.tc_handler.send_cb_call_args[0] + pus_entry = cast_wrapper.to_pus_tc_entry() + self.assertEqual( + pus_entry.pus_tc, PusTelecommand(service=service, subservice=subservice) + ) diff --git a/tests/test_cd.py b/tests/test_cd.py new file mode 100644 index 00000000..a4381910 --- /dev/null +++ b/tests/test_cd.py @@ -0,0 +1,28 @@ +import time +from datetime import timedelta +from unittest import TestCase +from tmtccmd.utility.countdown import Countdown + + +class CountdownTest(TestCase): + def test_basic(self): + test_cd = Countdown.from_millis(50) + self.assertTrue(test_cd.busy()) + self.assertFalse(test_cd.timed_out()) + self.assertTrue(test_cd.rem_time().total_seconds() * 1000 > 40) + time.sleep(0.05) + self.assertTrue(test_cd.timed_out()) + self.assertTrue(test_cd.rem_time() == timedelta()) + test_cd.timeout = timedelta(seconds=0.1) + self.assertTrue(test_cd.busy()) + self.assertFalse(test_cd.timed_out()) + time.sleep(0.1) + self.assertTrue(test_cd.timed_out()) + test_cd.reset(timedelta(seconds=0.5)) + self.assertTrue(test_cd.rem_time().total_seconds() * 1000 > 45) + self.assertTrue(test_cd.busy()) + self.assertFalse(test_cd.timed_out()) + test_cd.reset(timedelta(milliseconds=50)) + self.assertTrue(test_cd.busy()) + test_cd.time_out() + self.assertTrue(test_cd.timed_out()) diff --git a/tests/test_cfdp_filestore.py b/tests/test_cfdp_filestore.py new file mode 100644 index 00000000..8206ddaf --- /dev/null +++ b/tests/test_cfdp_filestore.py @@ -0,0 +1,66 @@ +import os.path +from pathlib import Path +import shutil +import tempfile + +from pyfakefs.fake_filesystem_unittest import TestCase +from tmtccmd.cfdp.filestore import HostFilestore, FilestoreResult + + +class TestCfdpHostFilestore(TestCase): + def setUp(self): + self.setUpPyfakefs() + self.temp_dir = tempfile.gettempdir() + self.test_file_name_0 = Path(f"{self.temp_dir}/cfdp_unittest0.txt") + self.test_file_name_1 = Path(f"{self.temp_dir}/cfdp_unittest1.txt") + self.test_dir_name_0 = Path(f"{self.temp_dir}/cfdp_test_folder0") + self.test_dir_name_1 = Path(f"{self.temp_dir}/cfdp_test_folder1") + self.test_list_dir_name = Path(f"{self.temp_dir}/list-dir-test.txt") + + def test_filestore(self): + filestore = HostFilestore() + + res = filestore.create_file(self.test_file_name_0) + self.assertTrue(res == FilestoreResult.CREATE_SUCCESS) + self.assertTrue(self.test_file_name_0.exists()) + res = filestore.create_file(self.test_file_name_0) + self.assertEqual(res, FilestoreResult.CREATE_NOT_ALLOWED) + + res = filestore.delete_file(self.test_file_name_0) + self.assertEqual(res, FilestoreResult.DELETE_SUCCESS) + self.assertFalse(os.path.exists(self.test_file_name_0)) + res = filestore.delete_file(self.test_file_name_0) + self.assertTrue(res == FilestoreResult.DELETE_FILE_DOES_NOT_EXIST) + + filestore.create_file(self.test_file_name_0) + res = filestore.rename_file(self.test_file_name_0, self.test_file_name_1) + self.assertTrue(res == FilestoreResult.RENAME_SUCCESS) + self.assertTrue(os.path.exists(self.test_file_name_1)) + self.assertFalse(os.path.exists(self.test_file_name_0)) + res = filestore.delete_file(self.test_file_name_1) + self.assertTrue(res == FilestoreResult.DELETE_SUCCESS) + + res = filestore.create_directory(self.test_file_name_0) + self.assertTrue(res == FilestoreResult.CREATE_DIR_SUCCESS) + self.assertTrue(os.path.isdir(self.test_file_name_0)) + res = filestore.create_directory(self.test_file_name_0) + self.assertTrue(res == FilestoreResult.CREATE_DIR_CAN_NOT_BE_CREATED) + + res = filestore.delete_file(self.test_file_name_0) + self.assertTrue(res == FilestoreResult.DELETE_NOT_ALLOWED) + res = filestore.remove_directory(self.test_file_name_0) + self.assertTrue(res == FilestoreResult.REMOVE_DIR_SUCCESS) + + def test_list_dir(self): + filestore = HostFilestore() + tempdir = Path(tempfile.gettempdir()) + if os.path.exists(self.test_list_dir_name): + os.remove(self.test_list_dir_name) + # Do not delete, user can check file content now + res = filestore.list_directory( + dir_name=tempdir, target_file=self.test_list_dir_name + ) + self.assertTrue(res == FilestoreResult.SUCCESS) + + def tearDown(self): + pass diff --git a/tests/test_com_if.py b/tests/test_com_if.py new file mode 100644 index 00000000..3abf4a31 --- /dev/null +++ b/tests/test_com_if.py @@ -0,0 +1,66 @@ +import os +from pathlib import Path +from unittest import TestCase +from unittest.mock import patch + +from spacepackets.ecss import PusTelecommand +from tmtccmd.com_if.dummy import DummyComIF +from tmtccmd.com_if.utils import determine_com_if + + +class TestComIF(TestCase): + def setUp(self) -> None: + self.json_file = "test.json" + + def test_com_if_utils(self): + with patch("tmtccmd.com_if.utils.wrapped_prompt", side_effect=["0", "yes"]): + test_dict = {"test-com-if": ("Some more info", None)} + com_if = determine_com_if(test_dict, self.json_file, True) + self.assertEqual(com_if, "test-com-if") + with open(self.json_file) as file: + lines = file.readlines() + lines[0] = "{\n" + lines[1] = ' "com_if": "test-com-if"\n' + lines[2] = "}" + os.remove(self.json_file) + with patch("tmtccmd.com_if.utils.wrapped_prompt", side_effect=["0", "no"]): + test_dict = {"test-com-if": ("Some more info", None)} + com_if = determine_com_if(test_dict, self.json_file, True) + self.assertEqual(com_if, "test-com-if") + with open(self.json_file) as file: + lines = file.readlines() + lines[0] = "{}" + with patch( + "tmtccmd.com_if.utils.wrapped_prompt", + side_effect=["1", "0", "no"], + ): + test_dict = {"test-com-if": ("Some more info", None)} + com_if = determine_com_if(test_dict, self.json_file, True) + self.assertEqual(com_if, "test-com-if") + with patch( + "tmtccmd.com_if.utils.wrapped_prompt", + side_effect=["blub", "0", "no"], + ): + test_dict = {"test-com-if": ("Some more info", None)} + com_if = determine_com_if(test_dict, self.json_file, True) + self.assertEqual(com_if, "test-com-if") + + def test_dummy_if(self): + dummy_com_if = DummyComIF() + self.assertFalse(dummy_com_if.is_open()) + dummy_com_if.open() + self.assertTrue(dummy_com_if.is_open()) + self.assertFalse(dummy_com_if.initialized) + dummy_com_if.initialize() + self.assertTrue(dummy_com_if.initialized) + self.assertFalse(dummy_com_if.data_available()) + dummy_com_if.send(PusTelecommand(service=17, subservice=1).pack()) + self.assertTrue(dummy_com_if.data_available()) + replies = dummy_com_if.receive() + # Full verification set (acceptance, start and completion) and ping reply + self.assertEqual(len(replies), 4) + + def tearDown(self) -> None: + path = Path(self.json_file) + if path.exists(): + os.remove(path) diff --git a/src/tests/test_global_manager.py b/tests/test_global_manager.py similarity index 73% rename from src/tests/test_global_manager.py rename to tests/test_global_manager.py index 91b00bea..3bdd5894 100644 --- a/src/tests/test_global_manager.py +++ b/tests/test_global_manager.py @@ -11,6 +11,11 @@ CoreModeList, CoreGlobalIds, ) +from tmtccmd.core.globals_manager import ( + lock_global_pool, + unlock_global_pool, + set_lock_timeout, +) class TestGlobalManager(TestCase): @@ -28,17 +33,18 @@ def test_global_module(self): com_if_dict = get_glob_com_if_dict() self.assertTrue(com_if_dict["test"][0] == "Test Interface") - set_default_globals_pre_args_parsing(gui=False, tc_apid=0x02, tm_apid=0x03) + set_default_globals_pre_args_parsing(apid=0x02) result = check_and_set_core_mode_arg(mode_arg="udp") - self.assertTrue(result == CoreModeList.SEQUENTIAL_CMD_MODE) + self.assertTrue(result == CoreModeList.ONE_QUEUE_MODE) result = check_and_set_core_mode_arg(mode_arg="listener") self.assertTrue(get_global(CoreGlobalIds.MODE) == CoreModeList.LISTENER_MODE) self.assertTrue(result == CoreModeList.LISTENER_MODE) result = check_and_set_core_mode_arg(mode_arg="seqcmd") - self.assertTrue( - get_global(CoreGlobalIds.MODE) == CoreModeList.SEQUENTIAL_CMD_MODE - ) - self.assertTrue(result == CoreModeList.SEQUENTIAL_CMD_MODE) + self.assertTrue(get_global(CoreGlobalIds.MODE) == CoreModeList.ONE_QUEUE_MODE) + self.assertTrue(result == CoreModeList.ONE_QUEUE_MODE) + lock_global_pool() + unlock_global_pool() + set_lock_timeout(0.5) diff --git a/tests/test_printer.py b/tests/test_printer.py new file mode 100644 index 00000000..09fa7388 --- /dev/null +++ b/tests/test_printer.py @@ -0,0 +1,57 @@ +import os +from pathlib import Path +from unittest import TestCase + +from spacepackets.ccsds.time import CdsShortTimestamp +from spacepackets.ecss.pus_1_verification import ( + RequestId, + VerificationParams, + Subservices, +) + +from tmtccmd.tm.pus_1_verification import Service1TmExtended +from tmtccmd.pus.pus_17_test import pack_service_17_ping_command +from tmtccmd.logging import get_console_logger, LOG_DIR +from tmtccmd.logging.pus import ( + RegularTmtcLogWrapper, + RawTmtcRotatingLogWrapper, +) + + +# TODO: Use temp files to test loggers? +class TestPrintersLoggers(TestCase): + def setUp(self): + self.log_path = Path(LOG_DIR) + if not self.log_path.exists(): + self.log_path.mkdir() + self.regular_file_name = Path( + RegularTmtcLogWrapper.get_current_tmtc_file_name() + ) + self.logger = get_console_logger() + + def test_pus_loggers(self): + regular_tmtc_logger = RegularTmtcLogWrapper(self.regular_file_name) + raw_tmtc_log = RawTmtcRotatingLogWrapper(max_bytes=1024, backup_count=10) + pus_tc = pack_service_17_ping_command(ssc=0) + raw_tmtc_log.log_tc(pus_tc) + pus_tm = Service1TmExtended( + subservice=Subservices.TM_START_SUCCESS, + time=CdsShortTimestamp.init_from_current_time(), + verif_params=VerificationParams( + req_id=RequestId(pus_tc.packet_id, pus_tc.packet_seq_ctrl) + ), + ) + raw_tmtc_log.log_tm(pus_tm.pus_tm) + self.assertTrue(Path(self.regular_file_name).exists()) + regular_tmtc_logger.logger.info("Test") + # There should be 2 files now because 1024 bytes are not enough to accomate all info + self.assertTrue(Path(raw_tmtc_log.file_name).exists()) + self.assertTrue(Path(f"{raw_tmtc_log.file_name}.1").exists()) + + def test_print_functions(self): + pass + + def tearDown(self): + """Reset the hook object""" + if self.regular_file_name.exists(): + os.remove(self.regular_file_name) diff --git a/src/tests/test_pus.py b/tests/test_pus.py similarity index 76% rename from src/tests/test_pus.py rename to tests/test_pus.py index 72e6efbe..9986cf59 100755 --- a/src/tests/test_pus.py +++ b/tests/test_pus.py @@ -1,17 +1,15 @@ #!/usr/bin/env python3 from unittest import TestCase -from spacepackets.ccsds.spacepacket import get_space_packet_sequence_control from spacepackets.ccsds.time import CdsShortTimestamp -from spacepackets.ecss.conf import get_pus_tm_version, PusVersion, set_default_tm_apid from spacepackets.util import PrintFormats -from tmtccmd.tm.pus_17_test import Service17TMExtended +from tmtccmd.tm.pus_17_test import Service17TmExtended class TestTelemetry(TestCase): def test_generic_pus_c(self): - pus_17_telemetry = Service17TMExtended( + pus_17_telemetry = Service17TmExtended( subservice=1, ssc=36, time=CdsShortTimestamp.init_from_current_time(), @@ -22,13 +20,12 @@ def test_generic_pus_c(self): pus_17_telemetry = None def tm_func(raw_telemetry: bytearray): - return Service17TMExtended.unpack(raw_telemetry=raw_telemetry) + return Service17TmExtended.unpack(raw_telemetry=raw_telemetry) self.assertRaises(ValueError, tm_func, bytearray()) self.assertRaises(ValueError, tm_func, None) - pus_17_telemetry = Service17TMExtended.unpack(raw_telemetry=pus_17_raw) - self.assertTrue(get_pus_tm_version() == PusVersion.PUS_C) + pus_17_telemetry = Service17TmExtended.unpack(raw_telemetry=pus_17_raw) self.assertTrue(pus_17_telemetry.service == 17) self.assertTrue(pus_17_telemetry.apid == 0xEF) self.assertTrue(pus_17_telemetry.subservice == 1) @@ -46,14 +43,14 @@ def tm_func(raw_telemetry: bytearray): print(full_string) print(pus_17_telemetry) print(repr(pus_17_telemetry)) - self.assertTrue(pus_17_telemetry.pus_tm.packet_id == 0x8 << 8 | 0xEF) + self.assertTrue(pus_17_telemetry.pus_tm.packet_id.raw() == 0x8 << 8 | 0xEF) def test_list_functionality(self): - pus_17_telecommand = Service17TMExtended( + pus_17_telecommand = Service17TmExtended( subservice=1, ssc=36, time=CdsShortTimestamp.init_from_current_time() ) pus_17_raw = pus_17_telecommand.pack() - pus_17_telemetry = Service17TMExtended.unpack(raw_telemetry=pus_17_raw) + pus_17_telemetry = Service17TmExtended.unpack(raw_telemetry=pus_17_raw) header_list = [] content_list = [] diff --git a/tests/test_pus_verif_log.py b/tests/test_pus_verif_log.py new file mode 100644 index 00000000..e712ae02 --- /dev/null +++ b/tests/test_pus_verif_log.py @@ -0,0 +1,119 @@ +import os +from pathlib import Path +from unittest import TestCase + +from spacepackets.ecss import PusTelecommand +from spacepackets.ecss.pus_1_verification import ( + create_acceptance_success_tm, + create_start_success_tm, + create_step_success_tm, + StepId, + create_completion_success_tm, + create_acceptance_failure_tm, + FailureNotice, + ErrorCode, + create_start_failure_tm, +) +from spacepackets.ecss.pus_verificator import PusVerificator +from tmtccmd import get_console_logger +from tmtccmd.logging.pus import RegularTmtcLogWrapper +from tmtccmd.pus import VerificationWrapper + + +class TestPusVerifLog(TestCase): + def setUp(self) -> None: + self.log_file_name = RegularTmtcLogWrapper.get_current_tmtc_file_name() + + def test_console_log_success(self): + logger = get_console_logger() + wrapper = VerificationWrapper(PusVerificator(), logger, None) + self._test_success(wrapper) + + def test_console_log_success_without_colors(self): + logger = get_console_logger() + wrapper = VerificationWrapper(PusVerificator(), logger, None) + wrapper.with_colors = False + self._test_success(wrapper) + + def _test_success(self, wrapper: VerificationWrapper): + verificator = wrapper.verificator + tc = PusTelecommand(service=17, subservice=1, seq_count=0) + verificator.add_tc(tc) + srv_1_tm = create_acceptance_success_tm(tc) + res = verificator.add_tm(srv_1_tm) + wrapper.log_to_console(srv_1_tm, res) + srv_1_tm = create_start_success_tm(tc) + res = verificator.add_tm(srv_1_tm) + wrapper.log_to_console(srv_1_tm, res) + srv_1_tm = create_step_success_tm(tc, StepId.with_byte_size(1, 1)) + res = verificator.add_tm(srv_1_tm) + wrapper.log_to_console(srv_1_tm, res) + srv_1_tm = create_completion_success_tm(tc) + res = verificator.add_tm(srv_1_tm) + wrapper.log_to_console(srv_1_tm, res) + + def test_console_log_acc_failure(self): + logger = get_console_logger() + wrapper = VerificationWrapper(PusVerificator(), logger, None) + self._test_acc_failure(wrapper) + + def test_console_log_acc_failure_without_colors(self): + logger = get_console_logger() + wrapper = VerificationWrapper(PusVerificator(), logger, None) + wrapper.with_colors = False + self._test_acc_failure(wrapper) + + def _test_acc_failure(self, wrapper: VerificationWrapper): + verificator = wrapper.verificator + tc = PusTelecommand(service=17, subservice=1, seq_count=1) + verificator.add_tc(tc) + srv_1_tm = create_acceptance_failure_tm( + tc, failure_notice=FailureNotice(code=ErrorCode(pfc=8, val=1), data=bytes()) + ) + res = verificator.add_tm(srv_1_tm) + wrapper.log_to_console(srv_1_tm, res) + + def test_console_log_start_failure(self): + logger = get_console_logger() + wrapper = VerificationWrapper(PusVerificator(), logger, None) + verificator = wrapper.verificator + tc = PusTelecommand(service=17, subservice=1, seq_count=2) + verificator.add_tc(tc) + srv_1_tm = create_acceptance_failure_tm( + tc, failure_notice=FailureNotice(code=ErrorCode(pfc=8, val=1), data=bytes()) + ) + res = verificator.add_tm(srv_1_tm) + wrapper.log_to_console(srv_1_tm, res) + srv_1_tm = create_start_failure_tm( + tc, failure_notice=FailureNotice(code=ErrorCode(pfc=8, val=1), data=bytes()) + ) + res = verificator.add_tm(srv_1_tm) + wrapper.log_to_console(srv_1_tm, res) + + def test_file_logger(self): + tmtc_logger = RegularTmtcLogWrapper(self.log_file_name) + wrapper = VerificationWrapper(PusVerificator(), None, tmtc_logger.logger) + verificator = wrapper.verificator + tc = PusTelecommand(service=17, subservice=1, seq_count=0) + verificator.add_tc(tc) + srv_1_tm = create_acceptance_success_tm(tc) + res = verificator.add_tm(srv_1_tm) + wrapper.log_to_file(srv_1_tm, res) + srv_1_tm = create_start_success_tm(tc) + res = verificator.add_tm(srv_1_tm) + wrapper.log_to_file(srv_1_tm, res) + srv_1_tm = create_step_success_tm(tc, StepId.with_byte_size(1, 1)) + res = verificator.add_tm(srv_1_tm) + wrapper.log_to_file(srv_1_tm, res) + srv_1_tm = create_completion_success_tm(tc) + res = verificator.add_tm(srv_1_tm) + wrapper.log_to_file(srv_1_tm, res) + # Assert that 4 lines have been written + with open(self.log_file_name) as file: + all_lines = file.readlines() + self.assertEqual(len(all_lines), 4) + + def tearDown(self) -> None: + log_file = Path(self.log_file_name) + if log_file.exists(): + os.remove(log_file) diff --git a/tests/test_queue.py b/tests/test_queue.py new file mode 100644 index 00000000..4f4a5e43 --- /dev/null +++ b/tests/test_queue.py @@ -0,0 +1,77 @@ +import math +from collections import deque +from datetime import timedelta +from typing import cast +from unittest import TestCase + +from spacepackets.ecss import PusTelecommand +from tmtccmd.tc import WaitEntry, QueueEntryHelper + +# Required for eval calls +# noinspection PyUnresolvedReferences +from tmtccmd.tc import LogQueueEntry, RawTcEntry +from tmtccmd.tc.queue import QueueWrapper, QueueHelper + + +class TestTcQueue(TestCase): + def test_queue(self): + queue_wrapper = QueueWrapper(info=None, queue=deque()) + self.assertEqual(queue_wrapper.queue, deque()) + queue_helper = QueueHelper(queue_wrapper) + queue_helper.add_wait(timedelta(seconds=2)) + self.assertEqual(len(queue_wrapper.queue), 1) + wait_entry = cast(WaitEntry, queue_wrapper.queue.pop()) + self.assertTrue(wait_entry) + self.assertFalse(wait_entry.is_tc()) + self.assertEqual(wait_entry.wait_time.total_seconds(), 2.0) + self.assertEqual(len(queue_wrapper.queue), 0) + pus_cmd = PusTelecommand(service=17, subservice=1) + queue_helper.add_pus_tc(pus_cmd) + self.assertEqual(len(queue_wrapper.queue), 1) + queue_helper.add_log_cmd("Test String") + queue_helper.add_raw_tc(bytes([0, 1, 2])) + queue_helper.add_ccsds_tc(pus_cmd.to_space_packet()) + queue_helper.add_packet_delay(timedelta(seconds=3.0)) + print(queue_wrapper.queue) + self.assertEqual(len(queue_wrapper.queue), 5) + + pus_entry = queue_wrapper.queue.popleft() + self.assertTrue(pus_entry.is_tc()) + cast_wrapper = QueueEntryHelper(pus_entry) + pus_entry = cast_wrapper.to_pus_tc_entry() + self.assertEqual(pus_entry.pus_tc, pus_cmd) + self.assertTrue(pus_entry) + with self.assertRaises(TypeError): + cast_wrapper.to_wait_entry() + log_entry = queue_wrapper.queue.popleft() + self.assertFalse(log_entry.is_tc()) + cast_wrapper.base = log_entry + log_entry = cast_wrapper.to_log_entry() + self.assertTrue(log_entry) + with self.assertRaises(TypeError): + cast_wrapper.to_raw_tc_entry() + self.assertEqual(log_entry.log_str, "Test String") + test_obj = eval(f"{log_entry!r}") + self.assertEqual(test_obj.log_str, log_entry.log_str) + + raw_entry = queue_wrapper.queue.popleft() + self.assertTrue(raw_entry.is_tc()) + cast_wrapper.base = raw_entry + raw_entry = cast_wrapper.to_raw_tc_entry() + self.assertTrue(raw_entry) + self.assertEqual(raw_entry.tc, bytes([0, 1, 2])) + test_obj = eval(f"{raw_entry!r}") + self.assertEqual(raw_entry.tc, test_obj.tc) + + space_packet_entry = queue_wrapper.queue.popleft() + self.assertTrue(space_packet_entry.is_tc()) + cast_wrapper.base = space_packet_entry + space_packet_entry = cast_wrapper.to_space_packet_entry() + self.assertTrue(space_packet_entry) + self.assertTrue(space_packet_entry.space_packet, pus_cmd.to_space_packet()) + + packet_delay = queue_wrapper.queue.pop() + self.assertFalse(packet_delay.is_tc()) + cast_wrapper.base = packet_delay + packet_delay = cast_wrapper.to_packet_delay_entry() + self.assertEqual(packet_delay.delay_time.total_seconds(), 3.0) diff --git a/tests/test_seq_cnt_provider.py b/tests/test_seq_cnt_provider.py new file mode 100644 index 00000000..f9750f64 --- /dev/null +++ b/tests/test_seq_cnt_provider.py @@ -0,0 +1,63 @@ +import os +from pathlib import Path +import platform +from unittest import TestCase + +from tmtccmd.pus.seqcnt import FileSeqCountProvider +from tempfile import NamedTemporaryFile + + +class TestSeqCount(TestCase): + def setUp(self) -> None: + self.file_name = Path("seq_cnt.txt") + + def test_basic(self): + if platform.system() != "Windows": + with NamedTemporaryFile("w+t") as file: + file.write("0\n") + file.seek(0) + seq_cnt_provider = FileSeqCountProvider(Path(file.name)) + seq_cnt = seq_cnt_provider.current() + self.assertEqual(seq_cnt, 0) + self.assertEqual(next(seq_cnt_provider), 1) + self.assertEqual(seq_cnt_provider.next_seq_count(), 2) + file.seek(0) + file.write(f"{pow(2, 14) - 1}\n") + file.flush() + # Assert rollover + self.assertEqual(next(seq_cnt_provider), 0) + + def test_with_real_file(self): + seq_cnt_provider = FileSeqCountProvider(self.file_name) + self.assertTrue(self.file_name.exists()) + self.assertEqual(seq_cnt_provider.current(), 0) + self.assertEqual(next(seq_cnt_provider), 1) + pass + + def test_file_deleted_runtime(self): + seq_cnt_provider = FileSeqCountProvider(self.file_name) + self.assertTrue(self.file_name.exists()) + os.remove(self.file_name) + with self.assertRaises(FileNotFoundError): + next(seq_cnt_provider) + with self.assertRaises(FileNotFoundError): + seq_cnt_provider.current() + + def test_faulty_file_entry(self): + if platform.system() != "Windows": + with NamedTemporaryFile("w+t") as file: + file.write("-1\n") + file.seek(0) + seq_cnt_provider = FileSeqCountProvider(Path(file.name)) + with self.assertRaises(ValueError): + next(seq_cnt_provider) + file.write(f"{pow(2, 15)}\n") + file.seek(0) + file.flush() + seq_cnt_provider = FileSeqCountProvider(Path(file.name)) + with self.assertRaises(ValueError): + next(seq_cnt_provider) + + def tearDown(self) -> None: + if self.file_name.exists(): + os.remove(self.file_name) diff --git a/tests/test_seq_sender.py b/tests/test_seq_sender.py new file mode 100644 index 00000000..33d2c215 --- /dev/null +++ b/tests/test_seq_sender.py @@ -0,0 +1,171 @@ +import time +from collections import deque +from typing import cast +from unittest import TestCase +from datetime import datetime, timedelta +from unittest.mock import MagicMock, ANY + +from spacepackets.ecss import PusTelecommand +from tmtccmd.com_if import ComInterface +from tmtccmd.tc.ccsds_seq_sender import SequentialCcsdsSender, SenderMode +from tmtccmd.tc.handler import TcHandlerBase +from tmtccmd.tc.queue import QueueWrapper, QueueHelper, QueueEntryHelper + + +class TestSendReceive(TestCase): + def setUp(self) -> None: + self.queue_wrapper = QueueWrapper(info=None, queue=deque()) + self.queue_helper = QueueHelper(self.queue_wrapper) + self.tc_handler_mock = MagicMock(spec=TcHandlerBase) + self.com_if = MagicMock(spec=ComInterface) + self.seq_sender = SequentialCcsdsSender( + self.queue_wrapper, self.tc_handler_mock + ) + + def test_basic(self): + res = self.seq_sender.operation(self.com_if) + # Queue is empty initially + self.assertEqual(res.mode, SenderMode.DONE) + self.assertEqual(self.seq_sender.mode, SenderMode.DONE) + self.assertTrue(self.seq_sender.no_delay_remaining()) + self.queue_helper.add_raw_tc(bytes([0, 1, 2])) + # One queue entry which should be handled immediately + self.seq_sender.queue_wrapper = self.queue_wrapper + self.assertEqual(self.seq_sender.mode, SenderMode.BUSY) + # Is busy now, so does not accept new queue unless forced + with self.assertRaises(ValueError): + self.seq_sender.queue_wrapper = self.queue_wrapper + self.seq_sender.operation(self.com_if) + self.tc_handler_mock.send_cb.assert_called_with(ANY, self.com_if) + call_args = self.tc_handler_mock.send_cb.call_args + cast_wrapper = cast(QueueEntryHelper, call_args.args[0]) + raw_tc_entry = cast_wrapper.to_raw_tc_entry() + self.assertEqual(raw_tc_entry.tc, bytes([0, 1, 2])) + # Queue should be empty now + # Called twice for each operation call + self.assertEqual(self.tc_handler_mock.queue_finished_cb.call_count, 2) + self.assertFalse(self.queue_wrapper.queue) + self.assertEqual(self.seq_sender.mode, SenderMode.DONE) + self.queue_helper.add_raw_tc(bytes([3, 2, 1])) + self.seq_sender.resume() + self.assertEqual(self.seq_sender.mode, SenderMode.BUSY) + res = self.seq_sender.operation(self.com_if) + self.assertTrue(res.tc_sent) + call_args = self.tc_handler_mock.send_cb.call_args + cast_wrapper = cast(QueueEntryHelper, call_args.args[0]) + self.assertEqual(cast_wrapper.to_raw_tc_entry().tc, bytes([3, 2, 1])) + + def test_with_wait_entry(self): + wait_delay = 0.01 + self.queue_helper.add_raw_tc(bytes([3, 2, 1])) + self.queue_helper.add_wait(timedelta(seconds=wait_delay)) + self.queue_helper.add_raw_tc(bytes([1, 2, 3])) + # Resume call necessary + self.assertEqual(self.seq_sender.mode, SenderMode.DONE) + self.seq_sender.resume() + res = self.seq_sender.operation(self.com_if) + self.assertEqual(res.mode, SenderMode.BUSY) + self.assertTrue(res.tc_sent) + self.tc_handler_mock.send_cb.assert_called_with(ANY, self.com_if) + call_args = self.tc_handler_mock.send_cb.call_args + cast_wrapper = cast(QueueEntryHelper, call_args.args[0]) + self.assertEqual(cast_wrapper.to_raw_tc_entry().tc, bytes([3, 2, 1])) + self.assertTrue(self.seq_sender.no_delay_remaining()) + # 2 queue entries remaining + self.assertEqual(len(self.queue_helper.queue_wrapper.queue), 2) + # Now the wait entry should be handled + res = self.seq_sender.operation(self.com_if) + self.assertFalse(self.seq_sender.no_delay_remaining()) + self.assertFalse(res.tc_sent) + self.tc_handler_mock.send_cb.assert_called_with(ANY, self.com_if) + call_args = self.tc_handler_mock.send_cb.call_args + cast_wrapper = cast(QueueEntryHelper, call_args.args[0]) + self.assertEqual( + cast_wrapper.to_wait_entry().wait_time, timedelta(seconds=wait_delay) + ) + # Now no TCs should be sent for 10 ms + self.assertEqual(len(self.queue_helper.queue_wrapper.queue), 1) + self.assertEqual(res.mode, SenderMode.BUSY) + res = self.seq_sender.operation(self.com_if) + self.assertFalse(res.tc_sent) + self.assertEqual(len(self.queue_helper.queue_wrapper.queue), 1) + # After a delay, TC should be sent + time.sleep(wait_delay) + res = self.seq_sender.operation(self.com_if) + self.assertTrue(res.tc_sent) + self.assertEqual(len(self.queue_helper.queue_wrapper.queue), 0) + self.tc_handler_mock.send_cb.assert_called_with(ANY, self.com_if) + call_args = self.tc_handler_mock.send_cb.call_args + cast_wrapper = cast(QueueEntryHelper, call_args.args[0]) + self.assertEqual(cast_wrapper.to_raw_tc_entry().tc, bytes([1, 2, 3])) + + def test_interpacket_delay(self): + delay_ms = 20 + inter_packet_delay = timedelta(milliseconds=delay_ms) + ping_cmd = PusTelecommand(service=17, subservice=1) + self.queue_helper.add_pus_tc(ping_cmd) + self.queue_helper.add_packet_delay_ms(delay_ms) + self.queue_helper.add_ccsds_tc(ping_cmd.to_space_packet()) + self.queue_helper.add_raw_tc(bytes([0, 1, 2])) + # Send first TC, assert delay of 10 ms, then send last packet + res = self.seq_sender.operation(self.com_if) + self.assertEqual(res.longest_rem_delay, timedelta()) + self.assertTrue(res.tc_sent) + self.tc_handler_mock.send_cb.assert_called_with(ANY, self.com_if) + call_args = self.tc_handler_mock.send_cb.call_args + cast_wrapper = cast(QueueEntryHelper, call_args.args[0]) + self.assertEqual(cast_wrapper.to_pus_tc_entry().pus_tc, ping_cmd) + res = self.seq_sender.operation(self.com_if) + self.assertFalse(res.tc_sent) + self.tc_handler_mock.send_cb.assert_called_with(ANY, self.com_if) + call_args = self.tc_handler_mock.send_cb.call_args + cast_wrapper = cast(QueueEntryHelper, call_args.args[0]) + self.assertEqual( + cast_wrapper.to_packet_delay_entry().delay_time.total_seconds(), + inter_packet_delay.total_seconds(), + ) + self.assertTrue( + inter_packet_delay <= res.longest_rem_delay <= inter_packet_delay + ) + res = self.seq_sender.operation(self.com_if) + # No TC sent + self.assertFalse(res.tc_sent) + self.assertEqual(len(self.queue_wrapper.queue), 2) + time.sleep(inter_packet_delay.total_seconds()) + res = self.seq_sender.operation(self.com_if) + # TC sent + self.assertTrue(res.tc_sent) + self.assertEqual(len(self.queue_wrapper.queue), 1) + res = self.seq_sender.operation(self.com_if) + # No TC sent, delay after each packet + self.assertFalse(res.tc_sent) + self.assertEqual(len(self.queue_wrapper.queue), 1) + self.assertTrue( + 0.8 * inter_packet_delay < res.longest_rem_delay <= inter_packet_delay + ) + # Delay 10 ms + time.sleep(inter_packet_delay.total_seconds()) + res = self.seq_sender.operation(self.com_if) + self.assertTrue(res.tc_sent) + self.tc_handler_mock.send_cb.assert_called_with(ANY, self.com_if) + call_args = self.tc_handler_mock.send_cb.call_args + cast_wrapper = cast(QueueEntryHelper, call_args.args[0]) + self.assertEqual(cast_wrapper.to_raw_tc_entry().tc, bytes([0, 1, 2])) + + def test_delay_at_end(self): + delay_at_end = timedelta(milliseconds=20) + self.queue_helper.add_raw_tc(bytes([3, 2, 1])) + self.queue_helper.add_wait(delay_at_end) + self.seq_sender.resume() + res = self.seq_sender.operation(self.com_if) + self.assertTrue(res.tc_sent) + self.assertEqual(res.longest_rem_delay, timedelta()) + res = self.seq_sender.operation(self.com_if) + self.assertFalse(res.tc_sent) + self.assertFalse(self.seq_sender.no_delay_remaining()) + self.assertTrue(0.8 * delay_at_end < res.longest_rem_delay <= delay_at_end) + self.assertEqual(self.seq_sender.mode, SenderMode.BUSY) + time.sleep(delay_at_end.total_seconds()) + self.assertTrue(self.seq_sender.no_delay_remaining()) + self.seq_sender.operation(self.com_if) + self.assertEqual(self.seq_sender.mode, SenderMode.DONE) diff --git a/tests/test_tm_handler.py b/tests/test_tm_handler.py new file mode 100644 index 00000000..0bf0a4e5 --- /dev/null +++ b/tests/test_tm_handler.py @@ -0,0 +1,59 @@ +from collections import deque +from unittest import TestCase +from unittest.mock import MagicMock + +from spacepackets.ecss import PusTelemetry +from tmtccmd.tm import ( + SpecificApidHandlerBase, + CcsdsTmHandler, + GenericApidHandlerBase, +) +from tmtccmd.com_if import ComInterface +from tmtccmd.tm.ccsds_tm_listener import CcsdsTmListener + + +class ApidHandler(SpecificApidHandlerBase): + def __init__(self, apid: int): + super().__init__(apid, None) + self.was_called = False + self.called_times = 0 + self.packet_queue = deque() + + def handle_tm(self, packet: bytes, user_args: any): + if not self.was_called: + self.was_called = True + self.called_times += 1 + self.packet_queue.appendleft(packet) + + +class TestTmHandler(TestCase): + def test_basic(self): + tm_handler = ApidHandler(0x01) + com_if = MagicMock(specs=ComInterface) + unknown_handler = MagicMock(specs=GenericApidHandlerBase) + ccsds_handler = CcsdsTmHandler(unknown_handler) + ccsds_handler.add_apid_handler(tm_handler) + tm_listener = CcsdsTmListener(tm_handler=ccsds_handler) + handled_packets = tm_listener.operation(com_if) + self.assertEqual(handled_packets, 0) + self.assertTrue(ccsds_handler.has_apid(0x01)) + tm0_raw = PusTelemetry(service=1, subservice=12, apid=0x01).pack() + tm1_raw = PusTelemetry(service=5, subservice=1, apid=0x01).pack() + com_if.receive.return_value = [tm0_raw] + handled_packets = tm_listener.operation(com_if) + self.assertEqual(handled_packets, 1) + self.assertTrue(tm_handler.was_called) + self.assertEqual(tm_handler.called_times, 1) + self.assertEqual(tm_handler.packet_queue.pop(), tm0_raw) + com_if.receive.return_value = [tm0_raw, tm1_raw] + handled_packets = tm_listener.operation(com_if) + self.assertEqual(handled_packets, 2) + self.assertEqual(tm_handler.called_times, 3) + self.assertEqual(handled_packets, 2) + self.assertEqual(tm_handler.packet_queue.pop(), tm0_raw) + self.assertEqual(tm_handler.packet_queue.pop(), tm1_raw) + unknown_apid = PusTelemetry(service=1, subservice=12, apid=0x02).pack() + com_if.receive.return_value = [unknown_apid] + handled_packets = tm_listener.operation(com_if) + self.assertEqual(handled_packets, 1) + unknown_handler.handle_tm.assert_called_once() diff --git a/tmtccmd/__init__.py b/tmtccmd/__init__.py new file mode 100644 index 00000000..fe95dbc3 --- /dev/null +++ b/tmtccmd/__init__.py @@ -0,0 +1,186 @@ +"""Contains core methods called by entry point files to setup and start a tmtccmd application""" +import sys +import os +from datetime import timedelta +from typing import Union, cast, Optional + +from tmtccmd.core.ccsds_backend import CcsdsTmtcBackend +from tmtccmd.core.base import FrontendBase +from tmtccmd.tm.ccsds_tm_listener import CcsdsTmListener +from tmtccmd.config import TmTcCfgHookBase, backend_mode_conversion, SetupWrapper +from tmtccmd.core.ccsds_backend import BackendBase +from tmtccmd.tm import TmTypes, TmHandlerBase, CcsdsTmHandler +from tmtccmd.core.globals_manager import update_global +from tmtccmd.logging import get_console_logger +from tmtccmd.config.globals import set_default_globals_pre_args_parsing +from tmtccmd.core import ModeWrapper +from tmtccmd.tc import DefaultProcedureInfo +from tmtccmd.tc.handler import TcHandlerBase + +VERSION_MAJOR = 3 +VERSION_MINOR = 0 +VERSION_REVISION = 0 + +# I think this needs to be in string representation to be parsed so we can't +# use a formatted string here. +__version__ = "3.0.0rc1" + + +LOGGER = get_console_logger() + +__SETUP_WAS_CALLED = False +__SETUP_FOR_GUI = False + + +def version() -> str: + return __version__ + + +def setup(setup_args: SetupWrapper): + """This function needs to be called first before running the TMTC commander core. The setup + arguments encapsulate all required arguments for the TMTC commander. + + :param setup_args: Setup arguments + """ + global __SETUP_WAS_CALLED, __SETUP_FOR_GUI + + if os.name == "nt": + import colorama + + colorama.init() + if setup_args.params.use_gui: + set_default_globals_pre_args_parsing(setup_args.params.apid) + if not setup_args.params.use_gui: + __handle_cli_args_and_globals(setup_args) + __SETUP_FOR_GUI = setup_args.params.use_gui + __SETUP_WAS_CALLED = True + + +def start( + tmtc_backend: BackendBase, + hook_obj: TmTcCfgHookBase, + tmtc_frontend: Optional[FrontendBase] = None, + app_name: str = "TMTC Commander", +): + """This is the primary function to run the TMTC commander. Users should call this function to + start the TMTC commander. Please note that :py:func:`setup` needs to be + called before this function. You also need to build a TMTC backend + instance and pass it to this call. You can use :py:func:`create_default_tmtc_backend` + to create a generic backend. + + :param tmtc_backend: Custom backend can be passed here. Otherwise, a default backend + will be created + :param hook_obj: + :param tmtc_frontend: Custom frontend can be passed here. Otherwise, a default frontend + will be created + :param app_name: Name of application. Will be displayed in GUI + :raises RunTimeError: if :py:func:`setup` was not called before + :return: + """ + global __SETUP_WAS_CALLED, __SETUP_FOR_GUI + if not __SETUP_WAS_CALLED: + LOGGER.warning("setup_tmtccmd was not called first. Call it first") + sys.exit(1) + if __SETUP_FOR_GUI: + __start_tmtc_commander_qt_gui( + tmtc_frontend=tmtc_frontend, + hook_obj=hook_obj, + tmtc_backend=tmtc_backend, + app_name=app_name, + ) + else: + __start_tmtc_commander_cli(tmtc_backend=tmtc_backend) + + +def init_printout(use_gui: bool): + if use_gui: + print(f"-- tmtccmd v{version()} GUI Mode --") + else: + print(f"-- tmtccmd v{version()} CLI Mode --") + + +# TODO: Remove globals altogether +def __handle_cli_args_and_globals(setup_args: SetupWrapper): + set_default_globals_pre_args_parsing(setup_args.params.apid) + + +def __start_tmtc_commander_cli(tmtc_backend: BackendBase): + tmtc_backend.open_com_if() + + +def __start_tmtc_commander_qt_gui( + tmtc_backend: BackendBase, + hook_obj: TmTcCfgHookBase, + tmtc_frontend: Union[None, FrontendBase] = None, + app_name: str = "TMTC Commander", +): + global __SETUP_WAS_CALLED + try: + from PyQt5.QtWidgets import QApplication + + if not __SETUP_WAS_CALLED: + LOGGER.warning("setup_tmtccmd was not called first. Call it first") + sys.exit(1) + app = QApplication([app_name]) + if tmtc_frontend is None: + from tmtccmd.gui import TmTcFrontend + from tmtccmd.core.ccsds_backend import CcsdsTmtcBackend + + tmtc_frontend = TmTcFrontend( + hook_obj=hook_obj, + tmtc_backend=cast(CcsdsTmtcBackend, tmtc_backend), + app_name=app_name, + ) + tmtc_frontend.start(app) + except ImportError as e: + LOGGER.exception(e) + sys.exit(1) + + +def create_default_tmtc_backend( + setup_wrapper: SetupWrapper, tm_handler: TmHandlerBase, tc_handler: TcHandlerBase +) -> BackendBase: + """Creates a default TMTC backend instance which can be passed to the tmtccmd runner + + :param tc_handler: + :param setup_wrapper: + :param tm_handler: + :return: + """ + global __SETUP_WAS_CALLED + + from typing import cast + + if not __SETUP_WAS_CALLED: + LOGGER.warning("setup_tmtccmd was not called first. Call it first") + sys.exit(1) + if tm_handler is None: + LOGGER.warning( + "No TM Handler specified! Make sure to specify at least one TM handler" + ) + sys.exit(1) + else: + if tm_handler.get_type() == TmTypes.CCSDS_SPACE_PACKETS: + tm_handler = cast(CcsdsTmHandler, tm_handler) + com_if = setup_wrapper.hook_obj.assign_communication_interface( + com_if_key=setup_wrapper.params.com_if_id + ) + tm_listener = CcsdsTmListener(tm_handler) + mode_wrapper = ModeWrapper() + backend_mode_conversion(setup_wrapper.params.mode, mode_wrapper) + # The global variables are set by the argument parser. + tmtc_backend = CcsdsTmtcBackend( + com_if=com_if, + tm_listener=tm_listener, + tc_handler=tc_handler, + tc_mode=mode_wrapper.tc_mode, + tm_mode=mode_wrapper.tm_mode, + ) + tmtc_backend.inter_cmd_delay = timedelta( + seconds=setup_wrapper.params.tc_params.delay + ) + tmtc_backend.current_procedure = DefaultProcedureInfo( + setup_wrapper.params.def_proc_args.service, + setup_wrapper.params.def_proc_args.op_code, + ) + return tmtc_backend diff --git a/src/tests/__init__.py b/tmtccmd/cfdp/__init__.py similarity index 100% rename from src/tests/__init__.py rename to tmtccmd/cfdp/__init__.py diff --git a/tmtccmd/cfdp/filestore.py b/tmtccmd/cfdp/filestore.py new file mode 100644 index 00000000..d4f9ef38 --- /dev/null +++ b/tmtccmd/cfdp/filestore.py @@ -0,0 +1,199 @@ +import abc +import os +import shutil +import platform +from pathlib import Path + +from tmtccmd.logging import get_console_logger +from spacepackets.cfdp.tlv import FilestoreResponseStatusCode + +LOGGER = get_console_logger() + +FilestoreResult = FilestoreResponseStatusCode + + +class VirtualFilestore: + @abc.abstractmethod + def append_data_to_file( + self, file: Path, offset: int, data: bytes + ) -> FilestoreResponseStatusCode: + """This is not used as part of a filestore request, it is used to build up the received + file""" + LOGGER.warning("Appending to file not implemented in virtual filestore") + return FilestoreResponseStatusCode.NOT_PERFORMED + + @abc.abstractmethod + def create_file(self, file: Path) -> FilestoreResponseStatusCode: + LOGGER.warning("Creating file not implemented in virtual filestore") + return FilestoreResponseStatusCode.NOT_PERFORMED + + @abc.abstractmethod + def delete_file(self, file: Path) -> FilestoreResponseStatusCode: + LOGGER.warning("Deleting file not implemented in virtual filestore") + return FilestoreResponseStatusCode.NOT_PERFORMED + + @abc.abstractmethod + def rename_file( + self, _old_file: Path, _new_file: Path + ) -> FilestoreResponseStatusCode: + LOGGER.warning("Renaming file not implemented in virtual filestore") + return FilestoreResponseStatusCode.NOT_PERFORMED + + @abc.abstractmethod + def replace_file( + self, _replaced_file: Path, _source_file: Path + ) -> FilestoreResponseStatusCode: + LOGGER.warning("Replacing file not implemented in virtual filestore") + return FilestoreResponseStatusCode.NOT_PERFORMED + + @abc.abstractmethod + def create_directory(self, _dir_name: Path) -> FilestoreResponseStatusCode: + LOGGER.warning("Creating directory not implemented in virtual filestore") + return FilestoreResponseStatusCode.NOT_PERFORMED + + @abc.abstractmethod + def remove_directory( + self, _dir_name: Path, recursive: bool + ) -> FilestoreResponseStatusCode: + LOGGER.warning("Removing directory not implemented in virtual filestore") + return FilestoreResponseStatusCode.NOT_PERFORMED + + @abc.abstractmethod + def list_directory( + self, _dir_name: Path, _file_name: Path, _recursive: bool = False + ) -> FilestoreResponseStatusCode: + LOGGER.warning("Listing directory not implemented in virtual filestore") + return FilestoreResponseStatusCode.NOT_PERFORMED + + +class HostFilestore(VirtualFilestore): + def __init__(self): + pass + + @abc.abstractmethod + def append_data_to_file( + self, file: Path, offset: int, data: bytes + ) -> FilestoreResponseStatusCode: + """Primary function used to perform the CFDP Copy Procedure. This will also create a new + file as long as no other file with the same name exists + + :return: + - FilestoreResponseStatusCode.APPEND_FROM_DATA_FILE_NOT_EXISTS: File does not exist yet + - FilestoreResponseStatusCode.APPEND_FROM_DATA_INVALID_OFFSET: Invalid offset + """ + if not file.exists(): + return FilestoreResponseStatusCode.APPEND_FROM_DATA_FILE_NOT_EXISTS + with open(file, "r+b") as of: + file_size = file.stat().st_size + if offset > file_size: + return FilestoreResponseStatusCode.APPEND_FROM_DATA_INVALID_OFFSET + of.seek(offset) + of.write(data) + return FilestoreResponseStatusCode.SUCCESS + + def create_file(self, file: Path) -> FilestoreResponseStatusCode: + """Returns CREATE_NOT_ALLOWED if the file already exists""" + if file.exists(): + LOGGER.warning("File already exists") + return FilestoreResponseStatusCode.CREATE_NOT_ALLOWED + try: + file = open(file, "x") + file.close() + return FilestoreResponseStatusCode.CREATE_SUCCESS + except OSError: + LOGGER.exception(f"Creating file {file} failed") + return FilestoreResponseStatusCode.CREATE_NOT_ALLOWED + + def delete_file(self, file: Path) -> FilestoreResponseStatusCode: + if not file.exists(): + return FilestoreResponseStatusCode.DELETE_FILE_DOES_NOT_EXIST + if file.is_dir(): + return FilestoreResponseStatusCode.DELETE_NOT_ALLOWED + os.remove(file) + return FilestoreResponseStatusCode.DELETE_SUCCESS + + def rename_file( + self, old_file: Path, new_file: Path + ) -> FilestoreResponseStatusCode: + if old_file.is_dir() or new_file.is_dir(): + LOGGER.exception(f"{old_file} or {new_file} is a directory") + return FilestoreResponseStatusCode.RENAME_NOT_PERFORMED + if not old_file.exists(): + return FilestoreResponseStatusCode.RENAME_OLD_FILE_DOES_NOT_EXIST + if new_file.exists(): + return FilestoreResponseStatusCode.RENAME_NEW_FILE_DOES_EXIST + old_file.rename(new_file) + return FilestoreResponseStatusCode.RENAME_SUCCESS + + def replace_file( + self, replaced_file: Path, source_file: Path + ) -> FilestoreResponseStatusCode: + if replaced_file.is_dir() or source_file.is_dir(): + LOGGER.warning(f"{replaced_file} is a directory") + return FilestoreResponseStatusCode.REPLACE_NOT_ALLOWED + if not replaced_file.exists(): + return ( + FilestoreResponseStatusCode.REPLACE_FILE_NAME_ONE_TO_BE_REPLACED_DOES_NOT_EXIST + ) + if not source_file.exists(): + return ( + FilestoreResponseStatusCode.REPLACE_FILE_NAME_TWO_REPLACE_SOURCE_NOT_EXIST + ) + source_file.replace(replaced_file) + + def remove_directory( + self, dir_name: Path, recursive: bool = False + ) -> FilestoreResponseStatusCode: + if not dir_name.exists(): + LOGGER.warning(f"{dir_name} does not exist") + return FilestoreResponseStatusCode.REMOVE_DIR_DOES_NOT_EXIST + elif not dir_name.is_dir(): + LOGGER.warning(f"{dir_name} is not a directory") + return FilestoreResponseStatusCode.REMOVE_DIR_NOT_ALLOWED + if recursive: + shutil.rmtree(dir_name) + else: + try: + os.rmdir(dir_name) + return FilestoreResponseStatusCode.REMOVE_DIR_SUCCESS + except OSError: + LOGGER.exception(f"Removing directory {dir_name} failed") + return FilestoreResponseStatusCode.RENAME_NOT_PERFORMED + + def create_directory(self, dir_name: Path) -> FilestoreResponseStatusCode: + if dir_name.exists(): + # It does not really matter if the existing structure is a file or a directory + return FilestoreResponseStatusCode.CREATE_DIR_CAN_NOT_BE_CREATED + os.mkdir(dir_name) + return FilestoreResponseStatusCode.CREATE_DIR_SUCCESS + + def list_directory( + self, dir_name: Path, target_file: Path, recursive: bool = False + ) -> FilestoreResponseStatusCode: + """List a directory + + :param dir_name: Name of directory to list + :param target_file: The list will be written into this target file + :param recursive: + :return: + """ + if target_file.exists(): + open_flag = "a" + else: + open_flag = "w" + with open(target_file, open_flag) as of: + if platform.system() == "Linux" or platform.system() == "Darwin": + cmd = "ls -al" + elif platform.system() == "Windows": + cmd = "dir" + else: + LOGGER.warning( + f"Unknown OS {platform.system()}, do not know how to list directory" + ) + return FilestoreResponseStatusCode.NOT_PERFORMED + of.write(f"Contents of directory {dir_name} generated with '{cmd}':\n") + curr_path = os.getcwd() + os.chdir(dir_name) + os.system(f"{cmd} >> {target_file}") + os.chdir(curr_path) + return FilestoreResponseStatusCode.SUCCESS diff --git a/tmtccmd/cfdp/handler.py b/tmtccmd/cfdp/handler.py new file mode 100644 index 00000000..beaa881a --- /dev/null +++ b/tmtccmd/cfdp/handler.py @@ -0,0 +1,215 @@ +import enum +import abc +import struct +from typing import Optional, List + +from .filestore import VirtualFilestore +from .mib import LocalEntityCfg +from tmtccmd.logging import get_console_logger +from tmtccmd.com_if import ComInterface +from spacepackets.cfdp.pdu.metadata import MetadataPdu +from spacepackets.cfdp.conf import PduConfig +from spacepackets.cfdp.definitions import ( + TransmissionModes, + ChecksumTypes, + SegmentationControl, + Direction, + CrcFlag, + FileSize, +) +from spacepackets.cfdp.tlv import ( + FaultHandlerOverrideTlv, + FlowLabelTlv, + MessageToUserTlv, + FileStoreRequestTlv, +) + +LOGGER = get_console_logger() + + +class CfdpRequest(enum.Enum): + PUT = 0 + REPORT = 1 + CANCEL = 2 + SUSPEND = 3 + RESUME = 4 + + +class CfdpIndication(enum.Enum): + TRANSACTION = 0 + EOF = 1 + FINISHED = 2 + METADATA = 3 + FILE_SEGMENT_RECV = 4 + REPORT = 5 + SUSPENDED = 6 + RESUMED = 7 + FAULT = 8 + ABANDONED = 9 + EOF_RECV = 10 + + +class PutRequest: + destination_id: bytes + source_file: str + dest_file: str + seg_ctrl: SegmentationControl + fault_handler_overrides: Optional[FaultHandlerOverrideTlv] = None + flow_label_tlv: Optional[FlowLabelTlv] = None + trans_mode: TransmissionModes + closure_requested: bool + msgs_to_user: Optional[List[MessageToUserTlv]] = None + fs_requests: Optional[List[FileStoreRequestTlv]] = None + + +class CfdpStates(enum.Enum): + IDLE = 0 + CRC_PROCEDURE = 1 + SENDING_METADATA = 2 + SENDING_FILE_DATA_PDUS = 3 + SENDING_EOF_DATA_PDU = 4 + SENDING_FINISH_PDU = 5 + SEND_ACK_PDU = 6 + + +class ByteFlowControl: + period: float + max_bytes: int + + +class BusyError(Exception): + pass + + +class SequenceNumberOverflow(Exception): + pass + + +class CfdpUserBase: + def __init__(self, vfs: VirtualFilestore): + self.vfs = vfs + + @abc.abstractmethod + def transaction_indication(self, code: CfdpIndication): + LOGGER.info(f"Received transaction indication {code}") + + +class CfdpHandler: + def __init__( + self, + cfg: LocalEntityCfg, + com_if: Optional[ComInterface], + cfdp_user: CfdpUserBase, + byte_flow_ctrl: ByteFlowControl, + ): + """ + + :param cfg: Local entity configuration + :param com_if: Communication interface used to send messages + :param cfdp_user: CFDP user which will receive indication messages and which also contains + the virtual filestore implementation + :param byte_flow_ctrl: Controls the number of bytes sent in a certain interval + The state machine will only send packets if the maximum number of specified bytes + is not exceeded in the specified time interval + """ + # The ID is going to be constant after initialization, store in separately + self.id = cfg.local_entity_id + self.cfg = cfg + self.com_if = com_if + self.cfdp_user = cfdp_user + self.state = CfdpStates.IDLE + self.seq_num = 0 + self.byte_flow_ctrl = byte_flow_ctrl + + self.__current_put_request: Optional[PutRequest] = None + + @property + def com_if(self): + return self.__com_if + + @com_if.setter + def com_if(self, com_if: ComInterface): + self.__com_if = com_if + + def state_machine(self): + """Perform the CFDP state machine + + :raises SequenceNumberOverflow: Overflow of sequence number occured. In this case, the + number will be reset but no operation will occured and the state machine needs + to be called again + """ + if self.state != CfdpStates.IDLE: + if self.state == CfdpStates.CRC_PROCEDURE: + # Skip this step for now + self.state = CfdpStates.SENDING_METADATA + if self.state == CfdpStates.SENDING_METADATA: + # TODO: CRC flag is derived from remote entity ID configuration + # TODO: Determine file size and check whether source file is valid + pdu_conf = PduConfig( + seg_ctrl=self.__current_put_request.seg_ctrl, + dest_entity_id=self.__current_put_request.destination_id, + source_entity_id=self.id, + crc_flag=CrcFlag.GLOBAL_CONFIG, + direction=Direction.TOWARDS_RECEIVER, + transaction_seq_num=self.__get_next_seq_num(), + file_size=FileSize.GLOBAL_CONFIG, + trans_mode=self.__current_put_request.trans_mode, + ) + self.send_metadata_pdu( + pdu_conf=pdu_conf, + dest_file=self.__current_put_request.dest_file, + source_file=self.__current_put_request.source_file, + closure_requested=False, + ) + self.state = CfdpStates.SENDING_FILE_DATA_PDUS + pass + + def __get_next_seq_num(self) -> bytes: + if self.cfg.length_seq_num == 1: + if self.seq_num == pow(2, 8) - 1: + LOGGER.warning("8-bit transaction sequence number overflowed!") + self.seq_num = 0 + raise SequenceNumberOverflow + self.seq_num += 1 + return bytes([self.seq_num]) + elif self.cfg.length_seq_num == 2: + if self.seq_num == pow(2, 16) - 1: + LOGGER.warning("16-bit transaction sequence number overflowed!") + self.seq_num = 0 + raise SequenceNumberOverflow + return struct.pack("!H", self.seq_num) + elif self.cfg.length_seq_num == 4: + if self.seq_num == pow(2, 32) - 1: + LOGGER.warning("32-bit transaction sequence number overflowed!") + self.seq_num = 0 + raise SequenceNumberOverflow + return struct.pack("!I", self.seq_num) + + def pass_packet(self, raw_tm_packet: bytes): + pass + + def put_request(self, put_request: PutRequest): + """A put request initiates a copy procedure. For now, only one put request at a time + is allowed""" + if self.state != CfdpStates.IDLE: + raise BusyError + self.__current_put_request = put_request + self.state = CfdpStates.CRC_PROCEDURE + + def send_metadata_pdu( + self, + pdu_conf: PduConfig, + source_file: str, + dest_file: str, + closure_requested: bool, + ): + metadata_pdu = MetadataPdu( + pdu_conf=pdu_conf, + file_size=0, + source_file_name=source_file, + dest_file_name=dest_file, + checksum_type=ChecksumTypes.NULL_CHECKSUM, + closure_requested=closure_requested, + ) + data = metadata_pdu.pack() + self.com_if.send(data=data) diff --git a/tmtccmd/cfdp/mib.py b/tmtccmd/cfdp/mib.py new file mode 100644 index 00000000..bf32a9be --- /dev/null +++ b/tmtccmd/cfdp/mib.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass +from typing import Callable +from spacepackets.cfdp.definitions import FaultHandlerCodes + +# User can specify a function which takes the fault handler code as an argument and returns nothing +FaultHandlerT = Callable[[FaultHandlerCodes], None] + + +@dataclass +class LocalEntityCfg: + local_entity_id: bytes + eof_sent_indication_required: bool + eof_recv_indication_required: bool + file_segment_recvd_required: bool + transaction_finished_indication_required: bool + suspended_indication_required: bool + resumed_indication_required: bool + default_fault_handlers: FaultHandlerT + length_seq_num: int = 2 + # I'm just going to assume that 255 possible IDs are sufficient for most applications + length_entity_ids: int = 1 diff --git a/src/tmtccmd/com_if/com_interface_base.py b/tmtccmd/com_if/__init__.py similarity index 82% rename from src/tmtccmd/com_if/com_interface_base.py rename to tmtccmd/com_if/__init__.py index 6d268004..2d9ad58f 100644 --- a/src/tmtccmd/com_if/com_interface_base.py +++ b/tmtccmd/com_if/__init__.py @@ -5,21 +5,20 @@ """ from abc import abstractmethod -from tmtccmd.tm.definitions import TelemetryListT -from tmtccmd.utility.tmtc_printer import FsfwTmTcPrinter +from tmtccmd.tm import TelemetryListT -class CommunicationInterface: +class ComInterface: """Generic form of a communication interface to separate communication logic from the underlying interface. """ - def __init__(self, com_if_key: str): + def __init__(self, com_if_id: str): self.valid = True - self.com_if_key = com_if_key + self.com_if_id = com_if_id def get_id(self) -> str: - return self.com_if_key + return self.com_if_id @abstractmethod def initialize(self, args: any = None) -> any: @@ -34,6 +33,12 @@ def open(self, args: any = None) -> None: :return: """ + @abstractmethod + def is_open(self) -> bool: + """Can be used to check whether the communication interface is open. This is useful if + opening a COM interface takes a longer time and is non-blocking + """ + @abstractmethod def close(self, args: any = None) -> None: """Closes the ComIF and releases any held resources (for example a Communication Port). diff --git a/tmtccmd/com_if/dummy.py b/tmtccmd/com_if/dummy.py new file mode 100644 index 00000000..60bbc667 --- /dev/null +++ b/tmtccmd/com_if/dummy.py @@ -0,0 +1,121 @@ +"""Dummy Virtual Communication Interface. Currently serves to use the TMTC program without needing +external hardware or an extra socket +""" +from typing import Optional + +from spacepackets.ecss.pus_1_verification import RequestId, VerificationParams +from spacepackets.ecss.tc import PusTelecommand + +from tmtccmd.com_if import ComInterface +from tmtccmd.config import CoreComInterfaces +from tmtccmd.tm import TelemetryListT +from tmtccmd.tm.pus_1_verification import Service1TmExtended +from tmtccmd.tm.pus_1_verification import Subservices as Pus1Subservices +from tmtccmd.tm.pus_17_test import Service17TmExtended +from tmtccmd.tm.pus_17_test import Subservices as Pus17Subservices +from tmtccmd.logging import get_console_logger + + +LOGGER = get_console_logger() + + +class DummyHandler: + def __init__(self): + self.last_tc: Optional[PusTelecommand] = None + self.next_telemetry_package = [] + self.current_ssc = 0 + self.reply_pending = False + + def pass_telecommand(self, data: bytearray): + self.last_tc = PusTelecommand.unpack(data) + self.reply_pending = True + self.generate_reply_package() + + def generate_reply_package(self): + """Generate a reply package. Currently, this only generates a reply for a ping telecommand.""" + if self.last_tc.service == 17: + if self.last_tc.subservice == 1: + tm_packer = Service1TmExtended( + subservice=Pus1Subservices.TM_ACCEPTANCE_SUCCESS, + seq_count=self.current_ssc, + verif_params=VerificationParams( + req_id=RequestId( + self.last_tc.packet_id, self.last_tc.packet_seq_ctrl + ) + ), + ) + + self.current_ssc += 1 + tm_packet_raw = tm_packer.pack() + self.next_telemetry_package.append(tm_packet_raw) + tm_packer = Service1TmExtended( + subservice=Pus1Subservices.TM_START_SUCCESS, + seq_count=self.current_ssc, + verif_params=VerificationParams( + req_id=RequestId( + self.last_tc.packet_id, self.last_tc.packet_seq_ctrl + ) + ), + ) + tm_packet_raw = tm_packer.pack() + self.next_telemetry_package.append(tm_packet_raw) + self.current_ssc += 1 + + tm_packer = Service17TmExtended(subservice=Pus17Subservices.TM_REPLY) + tm_packet_raw = tm_packer.pack() + self.next_telemetry_package.append(tm_packet_raw) + self.current_ssc += 1 + + tm_packer = Service1TmExtended( + subservice=Pus1Subservices.TM_COMPLETION_SUCCESS, + seq_count=self.current_ssc, + verif_params=VerificationParams( + req_id=RequestId( + self.last_tc.packet_id, self.last_tc.packet_seq_ctrl + ) + ), + ) + tm_packet_raw = tm_packer.pack() + self.next_telemetry_package.append(tm_packet_raw) + self.current_ssc += 1 + + def receive_reply_package(self) -> TelemetryListT: + if self.reply_pending: + return_list = self.next_telemetry_package.copy() + self.next_telemetry_package.clear() + self.reply_pending = False + return return_list + else: + return [] + + +class DummyComIF(ComInterface): + def __init__(self): + super().__init__(com_if_id=CoreComInterfaces.DUMMY.value) + self.dummy_handler = DummyHandler() + self._open = False + self.initialized = False + + def initialize(self, args: any = None) -> any: + self.initialized = True + + def open(self, args: any = None) -> None: + self._open = True + + def is_open(self) -> bool: + return self._open + + def close(self, args: any = None) -> None: + self._open = False + + def data_available(self, timeout: float = 0, parameters: any = 0): + if self.dummy_handler.reply_pending: + return True + return False + + def receive(self, parameters: any = 0) -> TelemetryListT: + return self.dummy_handler.receive_reply_package() + + def send(self, data: bytearray): + if data is not None: + self.dummy_handler.pass_telecommand(data) diff --git a/src/tmtccmd/com_if/qemu_com_if.py b/tmtccmd/com_if/qemu.py similarity index 98% rename from src/tmtccmd/com_if/qemu_com_if.py rename to tmtccmd/com_if/qemu.py index dc43e9a4..cbcfb6ec 100644 --- a/src/tmtccmd/com_if/qemu_com_if.py +++ b/tmtccmd/com_if/qemu.py @@ -27,10 +27,9 @@ from collections import deque from threading import Thread -from tmtccmd.com_if.com_interface_base import CommunicationInterface -from tmtccmd.tm.definitions import TelemetryListT -from tmtccmd.utility.tmtc_printer import FsfwTmTcPrinter -from tmtccmd.com_if.serial_com_if import SerialComIF, SerialCommunicationType +from tmtccmd.com_if import ComInterface +from tmtccmd.tm import TelemetryListT +from tmtccmd.com_if.serial import SerialComIF, SerialCommunicationType from tmtccmd.logging import get_console_logger from dle_encoder import DleEncoder, STX_CHAR, ETX_CHAR, DleErrorCodes @@ -61,18 +60,18 @@ def start_background_loop(loop: asyncio.AbstractEventLoop) -> None: loop.run_forever() -class QEMUComIF(CommunicationInterface): +class QEMUComIF(ComInterface): """ Specific Communication Interface implementation of the QEMU_SERIAL USART protocol for the TMTC software """ def __init__( self, - com_if_key: str, + com_if_id: str, serial_timeout: float, ser_com_type: SerialCommunicationType = SerialCommunicationType.FIXED_FRAME_BASED, ): - super().__init__(com_if_key=com_if_key) + super().__init__(com_if_id=com_if_id) self.serial_timeout = serial_timeout self.loop = asyncio.get_event_loop() self.number_of_packets = 0 diff --git a/src/tmtccmd/com_if/serial_utilities.py b/tmtccmd/com_if/ser_utils.py similarity index 99% rename from src/tmtccmd/com_if/serial_utilities.py rename to tmtccmd/com_if/ser_utils.py index 0283d1c6..e4fcae71 100644 --- a/src/tmtccmd/com_if/serial_utilities.py +++ b/tmtccmd/com_if/ser_utils.py @@ -4,7 +4,7 @@ import serial import serial.tools.list_ports from tmtccmd.logging import get_console_logger -from tmtccmd.utility.json_handler import ( +from tmtccmd.utility.json import ( check_json_file, JsonKeyNames, save_to_json_with_prompt, diff --git a/src/tmtccmd/com_if/serial_com_if.py b/tmtccmd/com_if/serial.py similarity index 93% rename from src/tmtccmd/com_if/serial_com_if.py rename to tmtccmd/com_if/serial.py index 9f98c53d..9eb8c82d 100644 --- a/src/tmtccmd/com_if/serial_com_if.py +++ b/tmtccmd/com_if/serial.py @@ -1,5 +1,4 @@ -"""Serial Communication Interface Implementation -""" +"""Serial Communication Interface Implementation""" import enum import threading import time @@ -10,9 +9,8 @@ import serial import serial.tools.list_ports -from tmtccmd.com_if.com_interface_base import CommunicationInterface -from tmtccmd.utility.tmtc_printer import FsfwTmTcPrinter -from tmtccmd.tm.definitions import TelemetryListT +from tmtccmd.com_if import ComInterface +from tmtccmd.tm import TelemetryListT from tmtccmd.logging import get_console_logger from dle_encoder import DleEncoder, STX_CHAR, ETX_CHAR, DleErrorCodes @@ -48,14 +46,12 @@ class SerialCommunicationType(enum.Enum): # pylint: disable=arguments-differ -class SerialComIF(CommunicationInterface): - """ - Communication Interface to use serial communication. This requires the PySerial library. - """ +class SerialComIF(ComInterface): + """Communication Interface to use serial communication. This requires the PySerial library.""" def __init__( self, - com_if_key: str, + com_if_id: str, com_port: str, baud_rate: int, serial_timeout: float, @@ -63,14 +59,12 @@ def __init__( ): """ Initiaze a serial communication handler. - :param tmtc_printer: TMTC printer object. Can be used for diagnostic purposes, but main - packet handling should be done by a separate thread. :param com_port: Specify COM port. :param baud_rate: Specify baud rate :param serial_timeout: Specify serial timeout :param ser_com_type: Specify how to handle serial reception """ - super().__init__(com_if_key=com_if_key) + super().__init__(com_if_id=com_if_id) self.com_port = com_port self.baud_rate = baud_rate @@ -135,6 +129,9 @@ def open(self, args: any = None) -> None: if self.ser_com_type == SerialCommunicationType.DLE_ENCODING: self.reception_thread.start() + def is_open(self) -> bool: + return self.serial is not None + def close(self, args: any = None) -> None: try: if self.ser_com_type == SerialCommunicationType.DLE_ENCODING: @@ -264,7 +261,6 @@ def parse_next_packets( end_index = frame_size return end_index next_packet_size = next_payload_len + 7 - # remaining_size = frame_size - start_index if next_packet_size > SERIAL_FRAME_LENGTH: LOGGER.error( diff --git a/src/tmtccmd/com_if/tcpip_tcp_com_if.py b/tmtccmd/com_if/tcp.py similarity index 88% rename from src/tmtccmd/com_if/tcpip_tcp_com_if.py rename to tmtccmd/com_if/tcp.py index d410430f..5f7932d2 100644 --- a/src/tmtccmd/com_if/tcpip_tcp_com_if.py +++ b/tmtccmd/com_if/tcp.py @@ -1,25 +1,18 @@ -""" -:file: tcpip_tcp_com_if.py -:date: 13.05.2021 -:brief: TCP communication interface -:author: R. Mueller -""" +"""TCP communication interface""" import socket import time import enum import threading import select from collections import deque -from typing import Union, Optional, Tuple +from typing import Optional, Tuple from spacepackets.ccsds.spacepacket import parse_space_packets from tmtccmd.logging import get_console_logger -from tmtccmd.config.definitions import CoreModeList -from tmtccmd.com_if.com_interface_base import CommunicationInterface -from tmtccmd.tm.definitions import TelemetryListT -from tmtccmd.utility.tmtc_printer import FsfwTmTcPrinter -from tmtccmd.config.definitions import EthernetAddressT +from tmtccmd.com_if import ComInterface +from tmtccmd.tm import TelemetryListT +from tmtccmd.com_if.tcpip_utils import EthernetAddressT from tmtccmd.utility.conf_util import acquire_timeout LOGGER = get_console_logger() @@ -34,7 +27,7 @@ class TcpCommunicationType(enum.Enum): SPACE_PACKETS = 0 -class TcpIpTcpComIF(CommunicationInterface): +class TcpComIF(ComInterface): """Communication interface for TCP communication.""" DEFAULT_LOCK_TIMEOUT = 0.4 @@ -42,31 +35,29 @@ class TcpIpTcpComIF(CommunicationInterface): def __init__( self, - com_if_key: str, + com_if_id: str, com_type: TcpCommunicationType, space_packet_ids: Tuple[int], tm_polling_freqency: float, target_address: EthernetAddressT, max_recv_size: int, max_packets_stored: int = 50, - init_mode: int = CoreModeList.LISTENER_MODE, ): """Initialize a communication interface to send and receive TMTC via TCP - :param com_if_key: + :param com_if_id: :param com_type: Communication Type. By default, it is assumed that space packets are sent via TCP :param space_packet_ids: 16 bit packet header for space packet headers. Used to detect the start of PUS packets :param tm_polling_freqency: Polling frequency in seconds """ - super().__init__(com_if_key=com_if_key) + super().__init__(com_if_id=com_if_id) self.com_type = com_type self.space_packet_ids = space_packet_ids self.tm_polling_frequency = tm_polling_freqency self.target_address = target_address self.max_recv_size = max_recv_size self.max_packets_stored = max_packets_stored - self.init_mode = init_mode self.connected = False self.__tcp_socket: Optional[socket.socket] = None @@ -93,10 +84,17 @@ def initialize(self, args: any = None) -> any: def open(self, args: any = None): self.__tm_thread_kill_signal.clear() - self.set_up_socket() + try: + self.set_up_socket() + except IOError as e: + LOGGER.exception("Issues setting up the TCP socket") + raise e self.set_up_tcp_thread() self.__tcp_conn_thread.start() + def is_open(self) -> bool: + return self.connected + def set_up_socket(self): if self.__tcp_socket is None: self.__tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -104,6 +102,8 @@ def set_up_socket(self): self.connected = True def set_up_tcp_thread(self): + # TODO: Do we really need a thread here? This could probably be implemented as a polled + # interface like UDP, using the select API if self.__tcp_conn_thread is None: self.__tcp_conn_thread = threading.Thread( target=self.__tcp_tm_client, daemon=True @@ -111,7 +111,7 @@ def set_up_tcp_thread(self): def close(self, args: any = None) -> None: self.__tm_thread_kill_signal.set() - if self.__tcp_conn_thread != None: + if self.__tcp_conn_thread is not None: if self.__tcp_conn_thread.is_alive(): self.__tcp_conn_thread.join(self.tm_polling_frequency) if self.connected: @@ -130,6 +130,8 @@ def send(self, data: bytearray): if not self.connected: self.set_up_socket() self.__tcp_socket.sendto(data, self.target_address) + except BrokenPipeError: + LOGGER.exception("Communication Interface setup might have failed") except ConnectionRefusedError or OSError: self.connected = False self.__tcp_socket.close() @@ -170,7 +172,7 @@ def __receive_tm_packets(self): ready = select.select([self.__tcp_socket], [], [], 0) if ready[0]: bytes_recvd = self.__tcp_socket.recv(self.max_recv_size) - if bytes_recvd == b'': + if bytes_recvd == b"": self.__close_tcp_socket() LOGGER.info("TCP server has been closed") return diff --git a/src/tmtccmd/com_if/tcpip_utilities.py b/tmtccmd/com_if/tcpip_utils.py similarity index 97% rename from src/tmtccmd/com_if/tcpip_utilities.py rename to tmtccmd/com_if/tcpip_utils.py index 43366cb4..db2bf9a1 100644 --- a/src/tmtccmd/com_if/tcpip_utilities.py +++ b/tmtccmd/com_if/tcpip_utils.py @@ -2,11 +2,10 @@ import socket import struct import enum +from typing import Tuple -from tmtccmd.config.definitions import EthernetAddressT -from tmtccmd.utility.json_handler import check_json_file +from tmtccmd.utility.json import check_json_file, JsonKeyNames from tmtccmd.logging import get_console_logger -from tmtccmd.utility.json_handler import JsonKeyNames LOGGER = get_console_logger() @@ -14,6 +13,9 @@ DEFAULT_MAX_RECV_SIZE = 1500 +EthernetAddressT = Tuple[str, int] + + class TcpIpType(enum.Enum): TCP = enum.auto() UDP = enum.auto() diff --git a/src/tmtccmd/com_if/tcpip_udp_com_if.py b/tmtccmd/com_if/udp.py similarity index 62% rename from src/tmtccmd/com_if/tcpip_udp_com_if.py rename to tmtccmd/com_if/udp.py index d870398d..0d76989d 100644 --- a/src/tmtccmd/com_if/tcpip_udp_com_if.py +++ b/tmtccmd/com_if/udp.py @@ -1,18 +1,12 @@ -""" -:file: tcpip_udp_com_if.py -:date: 13.05.2021 -:brief: UDP Communication Interface -:author: R. Mueller -""" +"""UDP Communication Interface""" import select import socket -from typing import Union +from typing import Optional from tmtccmd.logging import get_console_logger -from tmtccmd.com_if.com_interface_base import CommunicationInterface -from tmtccmd.tm.definitions import TelemetryListT -from tmtccmd.utility.tmtc_printer import FsfwTmTcPrinter -from tmtccmd.config.definitions import EthernetAddressT, CoreModeList +from tmtccmd.com_if import ComInterface +from tmtccmd.tm import TelemetryListT +from tmtccmd.com_if.tcpip_utils import EthernetAddressT LOGGER = get_console_logger() @@ -20,21 +14,17 @@ UDP_SEND_WIRETAPPING_ENABLED = False -# pylint: disable=abstract-method -# pylint: disable=arguments-differ -# pylint: disable=too-many-arguments -class TcpIpUdpComIF(CommunicationInterface): - """Communication interface for UDP communication.""" +class UdpComIF(ComInterface): + """Communication interface for UDP communication""" def __init__( self, - com_if_key: str, + com_if_id: str, tm_timeout: float, tc_timeout_factor: float, send_address: EthernetAddressT, max_recv_size: int, - recv_addr: Union[None, EthernetAddressT] = None, - init_mode: int = CoreModeList.LISTENER_MODE, + recv_addr: Optional[EthernetAddressT] = None, ): """Initialize a communication interface to send and receive UDP datagrams. :param tm_timeout: @@ -42,16 +32,14 @@ def __init__( :param send_address: :param max_recv_size: :param recv_addr: - :param tmtc_printer: Printer instance, can be passed optionally to allow packet debugging """ - super().__init__(com_if_key=com_if_key) + super().__init__(com_if_id=com_if_id) self.tm_timeout = tm_timeout self.tc_timeout_factor = tc_timeout_factor self.udp_socket = None self.send_address = send_address self.recv_addr = recv_addr self.max_recv_size = max_recv_size - self.init_mode = init_mode def __del__(self): try: @@ -64,7 +52,7 @@ def initialize(self, args: any = None) -> any: def open(self, args: any = None): self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - # Bind is possible but should not be necessary, and introduces risk of port alread + # Bind is possible but should not be necessary, and introduces risk of port already # being used. # See: https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-bind if self.recv_addr is not None: @@ -72,14 +60,11 @@ def open(self, args: any = None): f"Binding UDP socket to {self.recv_addr[0]} and port {self.recv_addr[1]}" ) self.udp_socket.bind(self.recv_addr) - # Set non-blocking because we use select. + # Set non-blocking because we use select self.udp_socket.setblocking(False) - if self.init_mode == CoreModeList.LISTENER_MODE: - from tmtccmd.pus.pus_17_test import pack_service17_ping_command - # Send ping command immediately so the reception address is known for UDP - ping_cmd = pack_service17_ping_command(ssc=0) - self.send(ping_cmd.pack()) + def is_open(self) -> bool: + return self.udp_socket is not None def close(self, args: any = None) -> None: if self.udp_socket is not None: @@ -101,15 +86,14 @@ def data_available(self, timeout: float = 0, parameters: any = 0) -> bool: return False def receive(self, poll_timeout: float = 0) -> TelemetryListT: + packet_list = [] if self.udp_socket is None: - return [] + return packet_list try: - ready = self.data_available(poll_timeout) - if ready: + while self.data_available(poll_timeout): data, sender_addr = self.udp_socket.recvfrom(self.max_recv_size) - packet_list = [bytearray(data)] - return packet_list - return [] + packet_list.append(bytearray(data)) + return packet_list except ConnectionResetError: LOGGER.warning("Connection reset exception occured!") return [] diff --git a/src/tmtccmd/com_if/com_if_utilities.py b/tmtccmd/com_if/utils.py similarity index 70% rename from src/tmtccmd/com_if/com_if_utilities.py rename to tmtccmd/com_if/utils.py index 0f62fff0..49e17d19 100644 --- a/src/tmtccmd/com_if/com_if_utilities.py +++ b/tmtccmd/com_if/utils.py @@ -1,15 +1,19 @@ import json from tmtccmd.logging import get_console_logger -from tmtccmd.utility.json_handler import check_json_file, JsonKeyNames -from tmtccmd.config.definitions import ComIFDictT +from tmtccmd.utility.conf_util import wrapped_prompt +from tmtccmd.utility.json import check_json_file, JsonKeyNames +from tmtccmd.config import ComIfDictT LOGGER = get_console_logger() -def determine_com_if(com_if_dict: ComIFDictT, json_cfg_path: str) -> str: +def determine_com_if( + com_if_dict: ComIfDictT, json_cfg_path: str, use_prompts: bool +) -> str: do_prompt_com_if = False - if not check_json_file(json_cfg_path=json_cfg_path): + com_if_string = "" + if not check_json_file(json_cfg_path): do_prompt_com_if = True if not do_prompt_com_if: with open(json_cfg_path, "r") as read: @@ -20,23 +24,26 @@ def determine_com_if(com_if_dict: ComIFDictT, json_cfg_path: str) -> str: except KeyError: do_prompt_com_if = True com_if_string = str(com_if_string) - if do_prompt_com_if: - com_if_string = prompt_com_if(com_if_dict=com_if_dict) - save_to_json = input( - "Do you want to store the communication interface? (y/n): " + if do_prompt_com_if and use_prompts: + com_if_string = prompt_com_if(com_if_dict) + save_to_json = wrapped_prompt( + "Do you want to store the communication interface? ([Y]/n): " ) - if save_to_json.lower() in ["y", "yes", "1"]: + if save_to_json.lower() in ["", "y", "yes", "1"]: store_com_if_json(com_if_string=com_if_string, json_cfg_path=json_cfg_path) return com_if_string -def prompt_com_if(com_if_dict: dict) -> str: +def prompt_com_if(com_if_dict: ComIfDictT) -> str: + com_if_string = "" while True: com_if_list = [] for index, com_if_value in enumerate(com_if_dict.items()): print(f"{index}: {com_if_value}") com_if_list.append(com_if_value) - com_if_key = input("Please enter the desired communication interface by key: ") + com_if_key = wrapped_prompt( + "Please enter the desired communication interface by key: " + ) if not com_if_key.isdigit(): print("Key is not a digit, try again") continue diff --git a/tmtccmd/config/__init__.py b/tmtccmd/config/__init__.py new file mode 100644 index 00000000..0340ad95 --- /dev/null +++ b/tmtccmd/config/__init__.py @@ -0,0 +1,89 @@ +"""Definitions for the TMTC commander core +""" +from typing import Optional + +from tmtccmd.core import ModeWrapper, TmMode, TcMode + +from .args import ( + SetupParams, + create_default_args_parser, + add_default_tmtccmd_args, + parse_default_tmtccmd_input_arguments, + DefProcedureParams, + ArgParserWrapper, +) +from .defs import ( + CoreModeList, + CoreModeStrings, + CoreComInterfaces, + CORE_COM_IF_DICT, + default_json_path, + CoreServiceList, + ComIfDictT, +) +from .prompt import prompt_op_code, prompt_service +from .tmtc import TmTcDefWrapper +from .hook import TmTcCfgHookBase + + +def backend_mode_conversion(mode: CoreModeList, mode_wrapper: ModeWrapper): + if mode == CoreModeStrings[CoreModeList.LISTENER_MODE]: + mode_wrapper.tm_mode = TmMode.LISTENER + mode_wrapper.tc_mode = TcMode.IDLE + elif mode == CoreModeStrings[CoreModeList.ONE_QUEUE_MODE]: + mode_wrapper.tm_mode = TmMode.LISTENER + mode_wrapper.tc_mode = TcMode.ONE_QUEUE + elif mode == CoreModeStrings[CoreModeList.MULTI_INTERACTIVE_QUEUE_MODE]: + mode_wrapper.tc_mode = TcMode.MULTI_QUEUE + mode_wrapper.tm_mode = TmMode.LISTENER + + +def get_global_hook_obj() -> Optional[TmTcCfgHookBase]: + """This function can be used to get the handle to the global hook object. + :return: + """ + from tmtccmd import get_console_logger + + logger = get_console_logger() + try: + from tmtccmd.core.globals_manager import get_global + from tmtccmd.config.definitions import CoreGlobalIds + + from typing import cast + + hook_obj_raw = get_global(CoreGlobalIds.TMTC_HOOK) + if hook_obj_raw is None: + logger.error("Hook object is invalid!") + return None + return cast(TmTcCfgHookBase, hook_obj_raw) + except ImportError: + logger.exception("Issues importing modules to get global hook handle!") + return None + except AttributeError: + logger.exception("Attribute error when trying to get global hook handle!") + return None + + +class SetupWrapper: + """This class encapsulates various important setup parameters required by tmtccmd components""" + + def __init__( + self, + hook_obj: TmTcCfgHookBase, + setup_params: SetupParams, + json_cfg_path: Optional[str] = None, + ): + """ + :param hook_obj: User hook object. Needs to be implemented by the user + :param setup_params: Optional helper wrapper which contains CLI arguments. + """ + self.hook_obj = hook_obj + self.json_cfg_path = json_cfg_path + self._params = setup_params + self.json_cfg_path = json_cfg_path + if json_cfg_path is None: + self.json_cfg_path = default_json_path() + + @property + def params(self): + return self._params diff --git a/tmtccmd/config/args.py b/tmtccmd/config/args.py new file mode 100644 index 00000000..04b080e0 --- /dev/null +++ b/tmtccmd/config/args.py @@ -0,0 +1,417 @@ +"""Argument parser module""" +import argparse +import sys +from typing import Optional, List +from dataclasses import dataclass + +from tmtccmd.config.prompt import prompt_op_code, prompt_service +from tmtccmd.logging import get_console_logger + +from .defs import CoreModeStrings, CoreModeList, CoreComInterfaces +from .hook import TmTcCfgHookBase + + +LOGGER = get_console_logger() + + +def get_default_descript_txt() -> str: + from tmtccmd.utility.conf_util import AnsiColors + + return ( + f"{AnsiColors.GREEN}TMTC Client Command Line Interface\n" + f"{AnsiColors.RESET}This application provides generic components to execute " + f"TMTC commanding\n" + ) + + +def create_default_args_parser( + descript_txt: Optional[str] = None, +) -> argparse.ArgumentParser: + if descript_txt is None: + descript_txt = get_default_descript_txt() + return argparse.ArgumentParser( + description=descript_txt, formatter_class=argparse.RawTextHelpFormatter + ) + + +@dataclass +class DefProcedureParams: + service: str + op_code: str + + +@dataclass +class TcParams: + delay: float = 0.0 + apid: int = 0 + + +@dataclass +class BackendParams: + mode: str = "" + com_if_id: str = "" + listener: bool = False + interactive: bool = False + + +@dataclass +class AppParams: + use_gui: bool = False + reduced_printout: bool = False + use_ansi_colors: bool = True + + +class SetupParams: + def __init__( + self, + def_proc_args: Optional[DefProcedureParams] = None, + tc_params: TcParams = TcParams(), + backend_params: BackendParams = BackendParams(), + app_params: AppParams = AppParams(), + ): + self.def_proc_args = def_proc_args + self.tc_params = tc_params + self.backend_params = backend_params + self.app_params = app_params + + @property + def apid(self): + return self.tc_params.apid + + @apid.setter + def apid(self, apid): + self.tc_params.apid = apid + + @property + def use_gui(self): + return self.app_params.use_gui + + @use_gui.setter + def use_gui(self, use_gui): + self.app_params.use_gui = use_gui + + @property + def mode(self): + return self.backend_params.mode + + @mode.setter + def mode(self, mode: str): + self.backend_params.mode = mode + + @property + def com_if_id(self): + return self.backend_params.com_if_id + + @com_if_id.setter + def com_if_id(self, com_if_id): + self.backend_params.com_if_id = com_if_id + + +def add_default_tmtccmd_args(parser: argparse.ArgumentParser): + add_default_mode_arguments(parser) + add_default_com_if_arguments(parser) + add_generic_arguments(parser) + add_cfdp_parser(parser) + + add_ethernet_arguments(parser) + + +def parse_default_tmtccmd_input_arguments( + parser: argparse.ArgumentParser, + print_known_args: bool = False, + print_unknown_args: bool = False, +) -> (argparse.Namespace, List[str]): + """Parses all input arguments + :return: Input arguments contained in a special namespace and accessable by args. + """ + + if len(sys.argv) == 1: + print("No input arguments specified. Run with -h to get list of arguments") + + args, unknown = parser.parse_known_args() + + if print_known_args: + LOGGER.info("Printing known arguments:") + for argument in vars(args): + LOGGER.debug(argument + ": " + str(getattr(args, argument))) + if print_unknown_args: + LOGGER.info("Printing unknown arguments:") + for argument in unknown: + LOGGER.info(argument) + + if len(unknown) > 0: + print(f"Unknown arguments detected: {unknown}") + return args, unknown + + +def add_cfdp_parser(arg_parser: argparse.ArgumentParser): + subparsers = arg_parser.add_subparsers( + title="CFDP", + description="CCSDS File Delivery Protocol commands", + help="CCDSDS File Delivery Commands", + dest="cfdp", + ) + cfdp = subparsers.add_parser("cfdp") + cfdp.add_argument("-p", "--proxy") + cfdp.add_argument( + "-f", "--file", dest="cfdp_file", help="CFDP target file", default=None + ) + cfdp.add_argument( + "-d", + "--dest", + dest="cfdp_dest", + help="CFDP file destination path", + default=None, + ) + + +def add_generic_arguments(arg_parser: argparse.ArgumentParser): + arg_parser.add_argument( + "-g", "--gui", help="Use GUI mode", action="store_true", default=False + ) + arg_parser.add_argument( + "-s", + "--service", + help="Procedure service code which is passed to the TC handler objects", + default=None, + ) + arg_parser.add_argument( + "-o", + "--op_code", + help="Procedcure operation code, which is passed to the TC packer functions", + default=None, + ) + arg_parser.add_argument( + "-l", + "--listener", + help="The backend will be configured to go into listener mode after " + "finishing the first queue", + action="store_true", + default=False, + ) + arg_parser.add_argument( + "-i", + "--interactive", + help="Enables interactive or multi-queue mode, where the backend will be configured " + "to handle multiple queues", + action="store_true", + default=False, + ) + arg_parser.add_argument( + "-d", + "--delay", + type=float, + help="Default inter-packet delay. Default: 3 seconds for one queue mode, " + "0 for interactive mode", + default=None, + ) + + +def add_default_mode_arguments(arg_parser: argparse.ArgumentParser): + from tmtccmd.config import CoreModeList, CoreModeStrings + + help_text = f"Core Modes. Default: {CoreModeStrings[CoreModeList.ONE_QUEUE_MODE]}\n" + one_q = ( + f"{CoreModeList.ONE_QUEUE_MODE} or " + f"{CoreModeStrings[CoreModeList.ONE_QUEUE_MODE]}: " + f"One Queue Command Mode\n" + ) + listener_help = ( + f"{CoreModeList.LISTENER_MODE} or {CoreModeStrings[CoreModeList.LISTENER_MODE]}: " + f"Listener Mode\n" + ) + multi_q = ( + f"{CoreModeList.MULTI_INTERACTIVE_QUEUE_MODE} or " + f"{CoreModeStrings[CoreModeList.MULTI_INTERACTIVE_QUEUE_MODE]}: " + f"Multi Queue and Interactive Command Mode\n" + ) + help_text += one_q + listener_help + multi_q + arg_parser.add_argument( + "-m", + "--mode", + type=str, + help=help_text, + default=CoreModeStrings[CoreModeList.ONE_QUEUE_MODE], + ) + + +def add_default_com_if_arguments(arg_parser: argparse.ArgumentParser): + from tmtccmd.config import CORE_COM_IF_DICT, CoreComInterfaces + + help_text = ( + "Core Communication Interface. If this is not specified, the commander core\n" + "will try to extract it from the JSON or prompt it from the user\n" + ) + dummy_line = f"{CORE_COM_IF_DICT[CoreComInterfaces.DUMMY.value]}: Dummy Interface\n" + udp_line = f"{CORE_COM_IF_DICT[CoreComInterfaces.UDP.value]}: " f"UDP client\n" + ser_dle_line = ( + f"{CORE_COM_IF_DICT[CoreComInterfaces.SERIAL_DLE.value]}: " + f"Serial with DLE transport layer\n" + ) + ser_fixed_line = ( + f"{CORE_COM_IF_DICT[CoreComInterfaces.SERIAL_FIXED_FRAME.value]}: " + f"Serial with fixed frames\n" + ) + ser_qemu_line = ( + f"{CORE_COM_IF_DICT[CoreComInterfaces.SERIAL_QEMU.value]}: " + f"QEMU serial interface\n" + ) + help_text += dummy_line + ser_dle_line + udp_line + ser_fixed_line + ser_qemu_line + arg_parser.add_argument( + "-c", + "--com_if", + type=str, + help=help_text, + default=CoreComInterfaces.UNSPECIFIED.value, + ) + + +def add_ethernet_arguments(arg_parser: argparse.ArgumentParser): + arg_parser.add_argument("--h-ip", help="Host (Computer) IP. Default:''", default="") + arg_parser.add_argument( + "--t-ip", help="Target IP. Default: Localhost 127.0.0.1", default="127.0.0.1" + ) + + +def args_to_params( + pargs: argparse.Namespace, + params: SetupParams, + hook_obj: TmTcCfgHookBase, + use_prompts: bool, +): + """If some arguments are unspecified, they are set here with (variable) default values. + + :param pargs: Parsed arguments from calling parse method + :param params: Setup parameter object which will be set by this function + :param hook_obj: + :param use_prompts: Specify whether terminal prompts are allowed to retrieve unspecified + arguments. For something like a GUI, it might make sense to disable this + :return: None + """ + from tmtccmd.com_if.utils import determine_com_if + + if pargs.gui is None: + params.app_params.use_gui = False + else: + params.app_params.use_gui = pargs.gui + if pargs.com_if is None or pargs.com_if == CoreComInterfaces.UNSPECIFIED.value: + params.com_if_id = determine_com_if( + hook_obj.get_com_if_dict(), hook_obj.json_cfg_path, use_prompts + ) + else: + # TODO: Check whether COM IF is valid? + params.com_if_id = pargs.com_if + if pargs.mode is None: + params.mode = CoreModeStrings[CoreModeList.ONE_QUEUE_MODE] + else: + params.mode = pargs.mode + tmtc_defs = hook_obj.get_tmtc_definitions() + params.def_proc_args = DefProcedureParams("0", "0") + if tmtc_defs is None: + LOGGER.warning("Invalid Service to Op-Code dictionary detected") + else: + if pargs.service is None: + if pargs.mode == CoreModeStrings[CoreModeList.ONE_QUEUE_MODE]: + if use_prompts: + print("No service argument (-s) specified, prompting from user") + # Try to get the service list from the hook base and prompt service from user + params.def_proc_args.service = prompt_service(tmtc_defs) + else: + params.def_proc_args.service = pargs.service + if pargs.op_code is None: + current_service = params.def_proc_args.service + if use_prompts: + params.def_proc_args.op_code = prompt_op_code( + tmtc_defs, current_service + ) + else: + params.def_proc_args.op_code = pargs.op_code + if pargs.delay is None: + if params.backend_params.mode == CoreModeStrings[CoreModeList.ONE_QUEUE_MODE]: + params.tc_params.delay = 3.0 + else: + params.tc_params.delay = 0.0 + else: + params.tc_params.delay = pargs.delay + if pargs.listener is None: + params.backend_params.listener = False + else: + params.backend_params.listener = pargs.listener + + +class ArgParserWrapper: + def __init__( + self, + hook_obj: TmTcCfgHookBase, + parser: Optional[argparse.ArgumentParser] = None, + descript_txt: Optional[str] = None, + ): + if parser is None: + self.args_parser = create_default_args_parser(descript_txt) + add_default_tmtccmd_args(self.args_parser) + else: + self.args_parser = parser + self.print_known_args = False + self.print_unknown_args = False + self.hook_obj = hook_obj + self.unknown_args = [""] + self.args_raw = None + self._parse_was_called = False + + def add_default_tmtccmd_args(self): + add_default_tmtccmd_args(self.args_parser) + + def parse(self): + """Parse all CLI arguments with the given argument parser""" + if not self._parse_was_called: + self.args_raw, self.unknown_args = parse_default_tmtccmd_input_arguments( + self.args_parser, + print_known_args=self.print_known_args, + print_unknown_args=self.print_unknown_args, + ) + self._parse_was_called = True + + @property + def use_gui(self): + """This only yields valid values if :py:meth:`parse` was called once""" + if not self._parse_was_called: + return False + return self.args_raw.gui + + def set_params(self, params: SetupParams): + """Set up the parameter object from the parsed arguments. This call auto-determines whether + prompts should be used depending on whether the GUI flag was passed or not. + + :raise Value Error: Parse function call missing + """ + if not self._parse_was_called: + raise ValueError("Call the parse function first") + if self.args_raw.gui: + self.set_params_without_prompts(params) + else: + self.set_params_with_prompts(params) + + def set_params_without_prompts(self, params: SetupParams): + if not self._parse_was_called: + raise ValueError("Call the parse function first") + args_to_params( + pargs=self.args_raw, + params=params, + hook_obj=self.hook_obj, + use_prompts=False, + ) + + def set_params_with_prompts(self, params: SetupParams): + if not self._parse_was_called: + raise ValueError("Call the parse function first") + try: + args_to_params( + pargs=self.args_raw, + params=params, + hook_obj=self.hook_obj, + use_prompts=True, + ) + except KeyboardInterrupt: + raise KeyboardInterrupt( + "Keyboard interrupt while converting CLI args to application parameters" + ) diff --git a/tmtccmd/config/cfdp.py b/tmtccmd/config/cfdp.py new file mode 100644 index 00000000..ea6d65a7 --- /dev/null +++ b/tmtccmd/config/cfdp.py @@ -0,0 +1,7 @@ +from spacepackets.cfdp.definitions import Direction + + +class CfdpCfg: + direction = Direction.TOWARDS_SENDER + source_file_name = "" + dest_file_name = "" diff --git a/src/tmtccmd/config/com_if.py b/tmtccmd/config/com_if.py similarity index 84% rename from src/tmtccmd/config/com_if.py rename to tmtccmd/config/com_if.py index 495b56f1..2b42a960 100644 --- a/src/tmtccmd/config/com_if.py +++ b/tmtccmd/config/com_if.py @@ -1,19 +1,20 @@ import sys from typing import Optional, Tuple -from tmtccmd.config.definitions import CoreGlobalIds, CoreComInterfaces +from tmtccmd.config import CoreComInterfaces +from tmtccmd.config.globals import CoreGlobalIds from tmtccmd.core.globals_manager import get_global, update_global -from tmtccmd.com_if.com_interface_base import CommunicationInterface -from tmtccmd.com_if.serial_com_if import ( +from tmtccmd.com_if import ComInterface +from tmtccmd.com_if.serial import ( SerialConfigIds, SerialCommunicationType, SerialComIF, ) -from tmtccmd.com_if.serial_utilities import determine_com_port, determine_baud_rate -from tmtccmd.com_if.tcpip_utilities import TcpIpConfigIds, TcpIpType +from tmtccmd.com_if.ser_utils import determine_com_port, determine_baud_rate +from tmtccmd.com_if.tcpip_utils import TcpIpConfigIds, TcpIpType from tmtccmd.logging import get_console_logger -from tmtccmd.com_if.tcpip_udp_com_if import TcpIpUdpComIF -from tmtccmd.com_if.tcpip_tcp_com_if import TcpIpTcpComIF, TcpCommunicationType +from tmtccmd.com_if.udp import UdpComIF +from tmtccmd.com_if.tcp import TcpComIF, TcpCommunicationType LOGGER = get_console_logger() @@ -22,21 +23,23 @@ def create_communication_interface_default( com_if_key: str, json_cfg_path: str, space_packet_ids: Tuple[int] = (0,), -) -> Optional[CommunicationInterface]: +) -> Optional[ComInterface]: """Return the desired communication interface object :param com_if_key: :param json_cfg_path: - :param space_packet_id: Can be used by communication interfaces as a start marker (e.g. TCP) + :param space_packet_ids: Can be used by communication interfaces as a start marker (e.g. TCP) :return: """ - from tmtccmd.com_if.dummy_com_if import DummyComIF - from tmtccmd.com_if.qemu_com_if import QEMUComIF + from tmtccmd.com_if.dummy import DummyComIF + from tmtccmd.com_if.qemu import QEMUComIF + if com_if_key == "": + LOGGER.warning("COM Interface key string is empty. Using dummy COM interface") try: if ( - com_if_key == CoreComInterfaces.TCPIP_UDP.value - or com_if_key == CoreComInterfaces.TCPIP_TCP.value + com_if_key == CoreComInterfaces.UDP.value + or com_if_key == CoreComInterfaces.TCP.value ): communication_interface = create_default_tcpip_interface( com_if_key=com_if_key, @@ -55,7 +58,7 @@ def create_communication_interface_default( serial_cfg = get_global(CoreGlobalIds.SERIAL_CONFIG) serial_timeout = serial_cfg[SerialConfigIds.SERIAL_TIMEOUT] communication_interface = QEMUComIF( - com_if_key=com_if_key, + com_if_id=com_if_key, serial_timeout=serial_timeout, ser_com_type=SerialCommunicationType.DLE_ENCODING, ) @@ -65,7 +68,7 @@ def create_communication_interface_default( dle_max_queue_len, dle_max_frame_size, serial_timeout ) else: - communication_interface = DummyComIF(com_if_key=com_if_key) + communication_interface = DummyComIF() if communication_interface is None: return communication_interface if not communication_interface.valid: @@ -75,9 +78,9 @@ def create_communication_interface_default( return communication_interface except ConnectionRefusedError: LOGGER.exception("TCP/IP connection refused") - if com_if_key == CoreComInterfaces.TCPIP_UDP.value: + if com_if_key == CoreComInterfaces.UDP.value: LOGGER.warning("Make sure that a UDP server is running") - if com_if_key == CoreComInterfaces.TCPIP_TCP.value: + if com_if_key == CoreComInterfaces.TCP.value: LOGGER.warning("Make sure that a TCP server is running") sys.exit(1) except (IOError, OSError): @@ -96,7 +99,7 @@ def default_tcpip_cfg_setup( :param space_packet_ids: Required if the TCP com interface needs to parse space packets :return: """ - from tmtccmd.com_if.tcpip_utilities import ( + from tmtccmd.com_if.tcpip_utils import ( determine_udp_send_address, determine_tcp_send_address, determine_recv_buffer_len, @@ -146,7 +149,7 @@ def create_default_tcpip_interface( com_if_key: str, json_cfg_path: str, space_packet_ids: Tuple[int] = (0,), -) -> Optional[CommunicationInterface]: +) -> Optional[ComInterface]: """Create a default serial interface. Requires a certain set of global variables set up. See :func:`default_tcpip_cfg_setup` for more details. @@ -156,9 +159,9 @@ def create_default_tcpip_interface( :return: """ communication_interface = None - if com_if_key == CoreComInterfaces.TCPIP_UDP.value: + if com_if_key == CoreComInterfaces.UDP.value: default_tcpip_cfg_setup(tcpip_type=TcpIpType.UDP, json_cfg_path=json_cfg_path) - elif com_if_key == CoreComInterfaces.TCPIP_TCP.value: + elif com_if_key == CoreComInterfaces.TCP.value: default_tcpip_cfg_setup( tcpip_type=TcpIpType.TCP, json_cfg_path=json_cfg_path, @@ -168,34 +171,30 @@ def create_default_tcpip_interface( send_addr = ethernet_cfg_dict[TcpIpConfigIds.SEND_ADDRESS] recv_addr = ethernet_cfg_dict[TcpIpConfigIds.RECV_ADDRESS] max_recv_size = ethernet_cfg_dict[TcpIpConfigIds.RECV_MAX_SIZE] - init_mode = get_global(CoreGlobalIds.MODE) - space_packet_id = ethernet_cfg_dict[TcpIpConfigIds.SPACE_PACKET_ID] - if com_if_key == CoreComInterfaces.TCPIP_UDP.value: - communication_interface = TcpIpUdpComIF( - com_if_key=com_if_key, + if com_if_key == CoreComInterfaces.UDP.value: + communication_interface = UdpComIF( + com_if_id=com_if_key, tm_timeout=get_global(CoreGlobalIds.TM_TIMEOUT), tc_timeout_factor=get_global(CoreGlobalIds.TC_SEND_TIMEOUT_FACTOR), send_address=send_addr, recv_addr=recv_addr, max_recv_size=max_recv_size, - init_mode=init_mode, ) - elif com_if_key == CoreComInterfaces.TCPIP_TCP.value: - communication_interface = TcpIpTcpComIF( - com_if_key=com_if_key, + elif com_if_key == CoreComInterfaces.TCP.value: + communication_interface = TcpComIF( + com_if_id=com_if_key, com_type=TcpCommunicationType.SPACE_PACKETS, space_packet_ids=space_packet_ids, tm_polling_freqency=0.5, target_address=send_addr, max_recv_size=max_recv_size, - init_mode=init_mode, ) return communication_interface def create_default_serial_interface( com_if_key: str, json_cfg_path: str -) -> Optional[CommunicationInterface]: +) -> Optional[ComInterface]: """Create a default serial interface. Requires a certain set of global variables set up. See :func:`set_up_serial_cfg` for more details. @@ -223,7 +222,7 @@ def create_default_serial_interface( else: ser_com_type = SerialCommunicationType.FIXED_FRAME_BASED communication_interface = SerialComIF( - com_if_key=com_if_key, + com_if_id=com_if_key, com_port=com_port, baud_rate=serial_baudrate, serial_timeout=serial_timeout, diff --git a/tmtccmd/config/defs.py b/tmtccmd/config/defs.py new file mode 100644 index 00000000..2f52996a --- /dev/null +++ b/tmtccmd/config/defs.py @@ -0,0 +1,82 @@ +import enum +from typing import Tuple, Dict + +# Com Interface Types +ComIfValueT = Tuple[str, any] +ComIfDictT = Dict[str, ComIfValueT] + + +def default_json_path() -> str: + return "tmtc_conf.json" + + +class CoreComInterfaces(enum.Enum): + DUMMY = "dummy" + SERIAL_DLE = "ser_dle" + UDP = "udp" + TCP = "tcp" + SERIAL_FIXED_FRAME = "ser_fixed" + SERIAL_QEMU = "ser_qemu" + UNSPECIFIED = "unspec" + + +CORE_COM_IF_DICT = { + CoreComInterfaces.DUMMY.value: ("Dummy Interface", None), + CoreComInterfaces.SERIAL_DLE.value: ("Serial Interace with DLE encoding", None), + CoreComInterfaces.UDP.value: ("TCP/IP with UDP datagrams", None), + CoreComInterfaces.TCP.value: ("TCP/IP with TCP", None), + CoreComInterfaces.SERIAL_FIXED_FRAME.value: ( + "Serial Interface with fixed size frames", + None, + ), + CoreComInterfaces.SERIAL_QEMU.value: ("Serial Interface using QEMU", None), + CoreComInterfaces.UNSPECIFIED.value: ("Unspecified", None), +} + + +# Mode options, set by args parser +class CoreModeList(enum.IntEnum): + """These are the core modes which will be translated to different TC and TM modes + for the CCSDS backend + + 1. ONE_QUEUE_MODE: This mode is optimized to handle one queue. It will configure the backend + to request program termination upon finishing the queue handling. This is also the + appropriate solution for single commands where the queue only consists of one telecommand. + 2. LISTENER_MODE: Only listen to TM + 3. MULTI_INTERACTIVE_QUEUE_MODE: + """ + + # + ONE_QUEUE_MODE = 0 + LISTENER_MODE = 1 + # This mode is optimized for the handling of multiple queues. It will configure the backend + # to request additional queues or a mode change from the user instead of requesting program + # termination + MULTI_INTERACTIVE_QUEUE_MODE = 3 + # The program will not do anything in this mode. This includes polling TM and sending any TCs + IDLE = 5 + + +CoreModeStrings = { + CoreModeList.ONE_QUEUE_MODE: "one-q", + CoreModeList.MULTI_INTERACTIVE_QUEUE_MODE: "multi-q", + CoreModeList.LISTENER_MODE: "listener", + CoreModeList.IDLE: "idle", +} + + +class CoreServiceList(enum.Enum): + SERVICE_2 = "2" + SERVICE_3 = "3" + SERVICE_5 = "5" + SERVICE_8 = "8" + SERVICE_9 = "9" + SERVICE_11 = "11" + SERVICE_17 = "17" + SERVICE_20 = "20" + SERVICE_23 = "23" + SERVICE_200 = "200" + + +DEFAULT_APID = 0xEF +DEBUG_MODE = False diff --git a/src/tmtccmd/config/globals.py b/tmtccmd/config/globals.py similarity index 70% rename from src/tmtccmd/config/globals.py rename to tmtccmd/config/globals.py index b8317a72..01519338 100644 --- a/src/tmtccmd/config/globals.py +++ b/tmtccmd/config/globals.py @@ -1,38 +1,69 @@ -import argparse import collections.abc +import enum import pprint -from typing import Union, List, Dict, Optional +from typing import Union, List, Dict - -from tmtccmd.logging import get_console_logger -from tmtccmd.utility.conf_util import check_args_in_dict from spacepackets.ecss.conf import ( - PusVersion, set_default_tc_apid, set_default_tm_apid, - set_pus_tc_version, - set_pus_tm_version, ) + +from tmtccmd.logging import get_console_logger from tmtccmd.core.globals_manager import update_global, get_global -from .definitions import ( - CoreGlobalIds, +from tmtccmd.config import ( CoreModeList, CoreServiceList, CoreModeStrings, - CoreComInterfacesDict, + CORE_COM_IF_DICT, CoreComInterfaces, - DEBUG_MODE, - ServiceOpCodeDictT, - OpCodeDictKeys, - ComIFDictT, - OpCodeEntryT, - OpCodeOptionsT, - OpCodeNameT, + ComIfDictT, ) -from tmtccmd.com_if.com_if_utilities import determine_com_if +from tmtccmd.config.tmtc import TmTcDefWrapper, OpCodeEntry + LOGGER = get_console_logger() -SERVICE_OP_CODE_DICT = dict() +DEF_WRAPPER = None + + +class CoreGlobalIds(enum.IntEnum): + """ + Numbers from 128 to 200 are reserved for core globals + """ + + # Object handles + TMTC_HOOK = 128 + COM_INTERFACE_HANDLE = 129 + TM_LISTENER_HANDLE = 130 + TMTC_PRINTER_HANDLE = 131 + TM_HANDLER_HANDLE = 132 + PRETTY_PRINTER = 133 + + # Parameters + JSON_CFG_PATH = 139 + MODE = 141 + CURRENT_SERVICE = 142 + COM_IF = 144 + OP_CODE = 145 + TM_TIMEOUT = 146 + SERVICE_OP_CODE_DICT = 147 + COM_IF_DICT = 148 + + # Miscellaneous + DISPLAY_MODE = 150 + USE_LISTENER_AFTER_OP = 151 + PRINT_HK = 152 + PRINT_TM = 153 + PRINT_RAW_TM = 154 + PRINT_TO_FILE = 155 + RESEND_TC = 156 + TC_SEND_TIMEOUT_FACTOR = 157 + + # Config dictionaries + USE_SERIAL = 160 + SERIAL_CONFIG = 161 + USE_ETHERNET = 162 + ETHERNET_CONFIG = 163 + END = 300 def set_json_cfg_path(json_cfg_path: str): @@ -43,21 +74,17 @@ def get_json_cfg_path() -> str: return get_global(CoreGlobalIds.JSON_CFG_PATH) -def set_glob_com_if_dict(custom_com_if_dict: ComIFDictT): - CoreComInterfacesDict.update(custom_com_if_dict) - update_global(CoreGlobalIds.COM_IF_DICT, CoreComInterfacesDict) +def set_glob_com_if_dict(custom_com_if_dict: ComIfDictT): + CORE_COM_IF_DICT.update(custom_com_if_dict) + update_global(CoreGlobalIds.COM_IF_DICT, CORE_COM_IF_DICT) -def get_glob_com_if_dict() -> ComIFDictT: +def get_glob_com_if_dict() -> ComIfDictT: return get_global(CoreGlobalIds.COM_IF_DICT) def set_default_globals_pre_args_parsing( - gui: bool, - tc_apid: int, - tm_apid: int, - pus_tc_version: PusVersion = PusVersion.PUS_C, - pus_tm_version: PusVersion = PusVersion.PUS_C, + apid: int, com_if_id: str = CoreComInterfaces.DUMMY.value, custom_com_if_dict=None, display_mode="long", @@ -67,10 +94,8 @@ def set_default_globals_pre_args_parsing( ): if custom_com_if_dict is None: custom_com_if_dict = dict() - set_default_tc_apid(tc_apid=tc_apid) - set_default_tm_apid(tm_apid=tm_apid) - set_pus_tc_version(pus_tc_version) - set_pus_tm_version(pus_tm_version) + set_default_tc_apid(tc_apid=apid) + set_default_tm_apid(tm_apid=apid) update_global(CoreGlobalIds.COM_IF, com_if_id) update_global(CoreGlobalIds.TC_SEND_TIMEOUT_FACTOR, tc_send_timeout_factor) update_global(CoreGlobalIds.TM_TIMEOUT, tm_timeout) @@ -112,11 +137,13 @@ def handle_mode_arg( def handle_com_if_arg( args, json_cfg_path: str, custom_com_if_dict: Dict[str, any] = None ): - all_com_ifs = CoreComInterfacesDict + from tmtccmd.com_if.utils import determine_com_if + + all_com_ifs = CORE_COM_IF_DICT if custom_com_if_dict is not None: - all_com_ifs = CoreComInterfacesDict.update(custom_com_if_dict) + all_com_ifs = CORE_COM_IF_DICT.update(custom_com_if_dict) try: - com_if_key = str(args.com_if) + com_if_key = str(args.com_if_id) except AttributeError: LOGGER.warning("No communication interface specified") LOGGER.warning("Trying to set from existing configuration..") @@ -156,6 +183,8 @@ def check_and_set_core_mode_arg( mode_arg: any, custom_modes_list: Union[None, List[Union[dict, collections.abc.Iterable]]] = None, ) -> int: + from tmtccmd.utility.conf_util import check_args_in_dict + """Checks whether the mode argument is contained inside the core mode list integer enumeration or a custom mode list integer which can be passed optionally. This function will set the single command mode as the global mode parameter if the passed mode @@ -192,9 +221,9 @@ def check_and_set_core_mode_arg( if mode_arg_invalid: LOGGER.warning( f"Passed mode argument might be invalid, " - f"setting to {CoreModeList.SEQUENTIAL_CMD_MODE}" + f"setting to {CoreModeList.ONE_QUEUE_MODE}" ) - mode_value = CoreModeList.SEQUENTIAL_CMD_MODE + mode_value = CoreModeList.ONE_QUEUE_MODE update_global(CoreGlobalIds.MODE, mode_value) return mode_value @@ -202,6 +231,8 @@ def check_and_set_core_mode_arg( def check_and_set_core_service_arg( service_arg: any, custom_service_list: collections.abc.Iterable = None ): + from tmtccmd.utility.conf_util import check_args_in_dict + in_enum, service_value = check_args_in_dict( param=service_arg, iterable=CoreServiceList, warning_hint="service" ) @@ -233,50 +264,22 @@ def check_and_set_core_service_arg( update_global(CoreGlobalIds.CURRENT_SERVICE, service_value) -def get_default_service_op_code_dict() -> ServiceOpCodeDictT: - global SERVICE_OP_CODE_DICT - service_op_code_dict = SERVICE_OP_CODE_DICT - if service_op_code_dict == dict(): - op_code_dict_srv_5 = { - "0": ("Event Test", {OpCodeDictKeys.TIMEOUT: 2.0}), - } - service_5_tuple = ("PUS Service 5 Event", op_code_dict_srv_5) - op_code_dict_srv_17 = { - "0": ("Ping Test", {OpCodeDictKeys.TIMEOUT: 2.2}), - } - service_17_tuple = ("PUS Service 17 Test", op_code_dict_srv_17) - - service_op_code_dict[CoreServiceList.SERVICE_5.value] = service_5_tuple - service_op_code_dict[CoreServiceList.SERVICE_17.value] = service_17_tuple - # SERVICE_OP_CODE_DICT = service_op_code_dict - return service_op_code_dict - - -def add_op_code_entry( - op_code_dict: OpCodeEntryT, - keys: OpCodeNameT, - info: str, - options: OpCodeOptionsT = None, -): - if isinstance(keys, str): - keys = [keys] - op_code_dict.update(OpCodeEntryT.fromkeys(keys, (info, options))) - - -def add_service_op_code_entry( - srv_op_code_dict: ServiceOpCodeDictT, - name: str, - info: str, - op_code_entry: OpCodeEntryT, -): - srv_op_code_dict.update({name: (info, op_code_entry)}) - - -def generate_op_code_options( - enter_listener_mode: bool = False, custom_timeout: Optional[float] = None -) -> dict: - op_code_opts = dict() - op_code_opts.update({OpCodeDictKeys.ENTER_LISTENER_MODE: enter_listener_mode}) - if custom_timeout is not None: - op_code_opts.update({OpCodeDictKeys.TIMEOUT: custom_timeout}) - return op_code_opts +def get_default_tmtc_defs() -> TmTcDefWrapper: + global DEF_WRAPPER + if DEF_WRAPPER is None: + DEF_WRAPPER = TmTcDefWrapper() + srv_5 = OpCodeEntry() + srv_5.add("0", "Event Test") + DEF_WRAPPER.add_service( + service_name=CoreServiceList.SERVICE_5.value, + info="PUS Service 5 Event", + op_code_entry=srv_5, + ) + srv_17 = OpCodeEntry() + srv_17.add("0", "Ping Test") + DEF_WRAPPER.add_service( + service_name=CoreServiceList.SERVICE_17.value, + info="PUS Service 17 Test", + op_code_entry=srv_17, + ) + return DEF_WRAPPER diff --git a/tmtccmd/config/hook.py b/tmtccmd/config/hook.py new file mode 100644 index 00000000..ec792e2d --- /dev/null +++ b/tmtccmd/config/hook.py @@ -0,0 +1,77 @@ +from typing import Optional +from abc import abstractmethod, ABC + +from tmtccmd.utility.obj_id import ObjectIdDictT +from tmtccmd.com_if import ComInterface + +from tmtccmd.core import BackendBase +from tmtccmd.utility.retval import RetvalDictT + +from .tmtc import TmTcDefWrapper +from .defs import default_json_path, CORE_COM_IF_DICT, ComIfDictT + + +class TmTcCfgHookBase(ABC): + """This hook allows users to adapt the TMTC commander core to the unique mission requirements. + It is used by implementing all abstract functions and then passing the instance to the + TMTC commander core. + """ + + def __init__(self, json_cfg_path: Optional[str] = None): + self.json_cfg_path = json_cfg_path + if self.json_cfg_path is None: + self.json_cfg_path = default_json_path() + + @abstractmethod + def get_object_ids(self) -> ObjectIdDictT: + from tmtccmd.config.objects import get_core_object_ids + + """The user can specify an object ID dictionary here mapping object ID bytearrays to a + list. This list could contain containing the string representation or additional + information about that object ID. + """ + return get_core_object_ids() + + @abstractmethod + def assign_communication_interface(self, com_if_key: str) -> Optional[ComInterface]: + """Assign the communication interface used by the TMTC commander to send and receive + TMTC with. + + :param com_if_key: String key of the communication interface to be created. + """ + from tmtccmd.config.com_if import create_communication_interface_default + + return create_communication_interface_default( + com_if_key=com_if_key, json_cfg_path=self.json_cfg_path + ) + + def get_com_if_dict(self) -> ComIfDictT: + return CORE_COM_IF_DICT + + @abstractmethod + def get_tmtc_definitions(self) -> TmTcDefWrapper: + """This is a dicitonary mapping services represented by strings to an operation code + dictionary. + + :return: + """ + from tmtccmd.config.globals import get_default_tmtc_defs + + return get_default_tmtc_defs() + + @abstractmethod + def perform_mode_operation(self, tmtc_backend: BackendBase, mode: int): + """Perform custom mode operations. + + :param tmtc_backend: + :param mode: + :return: + """ + pass + + def get_retval_dict(self) -> RetvalDictT: + from tmtccmd import get_console_logger + + logger = get_console_logger() + logger.info("No return value dictionary specified") + return dict() diff --git a/src/tmtccmd/config/objects.py b/tmtccmd/config/objects.py similarity index 74% rename from src/tmtccmd/config/objects.py rename to tmtccmd/config/objects.py index 60099875..db65513b 100644 --- a/src/tmtccmd/config/objects.py +++ b/tmtccmd/config/objects.py @@ -5,10 +5,10 @@ def get_core_object_ids() -> ObjectIdDictT: - """ - These are the object IDs for the tmtccmd core. The core will usually take care of + """These are the object IDs for the tmtccmd core. The core will usually take care of inserting these into the object manager during the program initialization. - :return: Dictionary of the core object IDs + + :return Dictionary of the core object IDs """ invalid_id = ObjectId.from_bytes(obj_id_as_bytes=INVALID_ID) invalid_id.name = "Invalid ID" diff --git a/tmtccmd/config/prompt.py b/tmtccmd/config/prompt.py new file mode 100644 index 00000000..f8aaa45d --- /dev/null +++ b/tmtccmd/config/prompt.py @@ -0,0 +1,101 @@ +from prompt_toolkit.completion import WordCompleter +from prompt_toolkit.shortcuts import CompleteStyle +import prompt_toolkit +from tmtccmd.config.tmtc import OpCodeEntry, TmTcDefWrapper +from tmtccmd.logging import get_console_logger + +LOGGER = get_console_logger() + + +def prompt_service(tmtc_defs: TmTcDefWrapper) -> str: + service_adjustment = 20 + info_adjustment = 30 + horiz_line_num = service_adjustment + info_adjustment + 3 + horiz_line = horiz_line_num * "-" + service_string = "Service".ljust(service_adjustment) + info_string = "Information".ljust(info_adjustment) + tmtc_defs.sort() + while True: + print(f" {horiz_line}") + print(f"|{service_string} | {info_string}|") + print(f" {horiz_line}") + srv_completer = build_service_word_completer(tmtc_defs) + for service_entry in tmtc_defs.defs.items(): + try: + adjusted_service_entry = service_entry[0].ljust(service_adjustment) + adjusted_service_info = service_entry[1][0].ljust(info_adjustment) + print(f"|{adjusted_service_entry} | {adjusted_service_info}|") + except AttributeError: + LOGGER.warning( + f"Error handling service entry {service_entry[0]}. Skipping.." + ) + print(f" {horiz_line}") + service_string = prompt_toolkit.prompt( + "Please select a service by specifying the key: ", + completer=srv_completer, + complete_style=CompleteStyle.MULTI_COLUMN, + ) + if service_string in tmtc_defs.defs: + LOGGER.info(f"Selected service: {service_string}") + return service_string + else: + LOGGER.warning("Invalid key, try again") + + +def build_service_word_completer( + tmtc_defs: TmTcDefWrapper, +) -> WordCompleter: + srv_list = [] + for service_entry in tmtc_defs.defs.items(): + srv_list.append(service_entry[0]) + srv_completer = WordCompleter(words=srv_list, ignore_case=True) + return srv_completer + + +def prompt_op_code(tmtc_defs: TmTcDefWrapper, service: str) -> str: + op_code_adjustment = 24 + info_adjustment = 56 + horz_line_num = op_code_adjustment + info_adjustment + 3 + horiz_line = horz_line_num * "-" + op_code_info_str = "Operation Code".ljust(op_code_adjustment) + info_string = "Information".ljust(info_adjustment) + while True: + print(f" {horiz_line}") + print(f"|{op_code_info_str} | {info_string}|") + print(f" {horiz_line}") + if service in tmtc_defs.defs: + op_code_entry = tmtc_defs.op_code_entry(service) + op_code_entry.sort() + completer = build_op_code_word_completer( + service=service, op_code_entry=op_code_entry + ) + for op_code in op_code_entry.op_code_dict.items(): + adjusted_op_code_entry = op_code[0].ljust(op_code_adjustment) + adjusted_op_code_info = op_code[1][0].ljust(info_adjustment) + print(f"|{adjusted_op_code_entry} | {adjusted_op_code_info}|") + print(f" {horiz_line}") + op_code_string = prompt_toolkit.prompt( + "Please select an operation code by specifying the key: ", + completer=completer, + complete_style=CompleteStyle.MULTI_COLUMN, + ) + if op_code_string in op_code_entry.op_code_dict.keys(): + LOGGER.info(f"Selected op code: {op_code_string}") + return op_code_string + else: + LOGGER.warning("Invalid key, try again") + else: + LOGGER.warning( + "Service not in dictionary. Setting default operation code 0" + ) + return "0" + + +def build_op_code_word_completer( + service: str, op_code_entry: OpCodeEntry +) -> WordCompleter: + op_code_list = [] + for op_code_entry in op_code_entry.op_code_dict.items(): + op_code_list.append(op_code_entry[0]) + op_code_completer = WordCompleter(words=op_code_list, ignore_case=True) + return op_code_completer diff --git a/tmtccmd/config/tmtc.py b/tmtccmd/config/tmtc.py new file mode 100644 index 00000000..3a87461a --- /dev/null +++ b/tmtccmd/config/tmtc.py @@ -0,0 +1,82 @@ +from typing import Union, List, Optional, Dict, Tuple + +ServiceNameT = str +ServiceInfoT = str +OpCodeNameT = Union[str, List[str]] +OpCodeInfoT = str + + +class OpCodeOptionBase: + def __init__(self): + pass + + +OpCodeDictT = Dict[str, Tuple[OpCodeInfoT, OpCodeOptionBase]] + + +class OpCodeEntry: + def __init__(self, init_dict: Optional[OpCodeDictT] = None): + if init_dict is not None: + self._op_code_dict = init_dict + else: + self._op_code_dict: OpCodeDictT = dict() + + def add( + self, + keys: OpCodeNameT, + info: str, + options: OpCodeOptionBase = OpCodeOptionBase(), + ): + if isinstance(keys, str): + keys = [keys] + self._op_code_dict.update(OpCodeDictT.fromkeys(keys, (info, options))) + + def sort(self): + self._op_code_dict = { + key: self._op_code_dict[key] for key in sorted(self._op_code_dict.keys()) + } + + def info(self, op_code: str) -> Optional[str]: + entry_tuple = self._op_code_dict.get(op_code) + if entry_tuple is not None: + return entry_tuple[0] + + def __repr__(self): + return f"{self.__class__.__name__}(init_dict={self._op_code_dict!r}" + + @property + def op_code_dict(self): + return self._op_code_dict + + +# It is possible to specify a service without any op codes +ServiceDictValueT = Optional[Tuple[ServiceInfoT, OpCodeEntry]] +ServiceOpCodeDictT = Dict[ServiceNameT, ServiceDictValueT] + + +class TmTcDefWrapper: + def __init__(self, init_defs: Optional[ServiceOpCodeDictT] = None): + if init_defs is None: + self.defs: ServiceOpCodeDictT = dict() + else: + self.defs = init_defs + + def __repr__(self): + return f"{self.__class__.__name__}(init_defs={self.defs!r}" + + def add_service( + self, + service_name: str, + info: str, + op_code_entry: OpCodeEntry, + ): + self.defs.update({service_name: (info, op_code_entry)}) + + def op_code_entry(self, service_name: str) -> Optional[OpCodeEntry]: + srv_entry = self.defs.get(service_name) + if srv_entry is not None: + return srv_entry[1] + return None + + def sort(self): + self.defs = {key: self.defs[key] for key in sorted(self.defs.keys())} diff --git a/tmtccmd/core/__init__.py b/tmtccmd/core/__init__.py new file mode 100644 index 00000000..c03e8ba2 --- /dev/null +++ b/tmtccmd/core/__init__.py @@ -0,0 +1,105 @@ +from abc import abstractmethod +import enum +from datetime import timedelta +from typing import Optional + +from tmtccmd.tc.ccsds_seq_sender import SeqResultWrapper, SenderMode + + +class TcMode(enum.IntEnum): + IDLE = 0 + ONE_QUEUE = 1 + MULTI_QUEUE = 2 + + +class TmMode(enum.IntEnum): + IDLE = 0 + LISTENER = 1 + + +class ModeWrapper: + def __init__(self): + self.tc_mode = TcMode.IDLE + self.tm_mode = TmMode.IDLE + + def __str__(self): + return f"{self.__class__.__name__}: tc_mode={self.tc_mode!r}, tm_mode={self.tm_mode!r}" + + +class BackendRequest(enum.IntEnum): + """These requests can be treated like recommendations on what to do after calling the backend + handler functions and the :py:meth:`BackendState.mode_to_req` function. + + Brief explanation of fields: + 1. NONE: No special recommendation + 2. TERMINATION_NO_ERROR: Will be returned for the One Queue mode after finishing queue handling. + 3. DELAY_IDLE: TC and TM mode are idle, so there is nothing to do + 4. DELAY_LISTENER: TC handling is not active but TM listening is active. Delay to + wait for new TM packets + 5. CALL_NEXT: It is recommended to call the handler functions immediately, for example to + handle the next entry in the TC queue + """ + + NONE = 0 + TERMINATION_NO_ERROR = 1 + DELAY_IDLE = 2 + DELAY_LISTENER = 3 + DELAY_CUSTOM = 4 + CALL_NEXT = 5 + + +class BackendState: + def __init__( + self, + mode_wrapper: ModeWrapper = ModeWrapper(), + req: BackendRequest = BackendRequest.NONE, + ): + self._mode_wrapper = mode_wrapper + self._req = req + self._recommended_delay = timedelta() + self._sender_res = SeqResultWrapper(SenderMode.DONE) + + @property + def next_delay(self): + return self._recommended_delay + + @property + def request(self): + return self._req + + @property + def sender_res(self): + return self._sender_res + + @property + def tc_mode(self): + return self._mode_wrapper.tc_mode + + @property + def tm_mode(self): + return self._mode_wrapper.tm_mode + + @property + def mode_wrapper(self): + return self._mode_wrapper + + +class BackendController: + def __init__(self): + self.next_tc_mode = TcMode.IDLE + self.next_tm_mode = TmMode.IDLE + + +class BackendBase: + @abstractmethod + def open_com_if(self): + """Start the backend. Raise RuntimeError on failure""" + pass + + @abstractmethod + def close_com_if(self): + pass + + @abstractmethod + def periodic_op(self, args: Optional[any]) -> BackendState: + pass diff --git a/src/tmtccmd/core/frontend_base.py b/tmtccmd/core/base.py similarity index 100% rename from src/tmtccmd/core/frontend_base.py rename to tmtccmd/core/base.py diff --git a/tmtccmd/core/ccsds_backend.py b/tmtccmd/core/ccsds_backend.py new file mode 100644 index 00000000..71539574 --- /dev/null +++ b/tmtccmd/core/ccsds_backend.py @@ -0,0 +1,252 @@ +import atexit +import sys +from collections import deque +from datetime import timedelta +from typing import Optional + +from tmtccmd.core import ( + BackendBase, + BackendState, + BackendRequest, + TcMode, + TmMode, +) +from tmtccmd.tc import TcProcedureBase, ProcedureHelper +from tmtccmd.tc.handler import TcHandlerBase, FeedWrapper +from tmtccmd.utility.exit_handler import keyboard_interrupt_handler +from tmtccmd.tc.queue import QueueWrapper +from tmtccmd.logging import get_console_logger +from tmtccmd.tc.ccsds_seq_sender import ( + SequentialCcsdsSender, + SenderMode, +) +from tmtccmd.tm.ccsds_tm_listener import CcsdsTmListener +from tmtccmd.com_if import ComInterface + +LOGGER = get_console_logger() + + +class NoValidProcedureSet(Exception): + pass + + +class CcsdsTmtcBackend(BackendBase): + """This is the primary class which handles TMTC reception and sending""" + + def __init__( + self, + tc_mode: TcMode, + tm_mode: TmMode, + com_if: ComInterface, + tm_listener: CcsdsTmListener, + tc_handler: TcHandlerBase, + ): + self._state = BackendState() + self._state.mode_wrapper.tc_mode = tc_mode + self._state.mode_wrapper.tm_mode = tm_mode + + self._com_if_active = False + self._tc_handler = tc_handler + + self._com_if = com_if + self._tm_listener = tm_listener + self.exit_on_com_if_init_failure = True + # This can be used to keep the TC mode in multi queue mode after finishing the handling + # of a queue + self.keep_multi_queue_mode = False + self._queue_wrapper = QueueWrapper(None, deque()) + self._seq_handler = SequentialCcsdsSender( + tc_handler=tc_handler, + queue_wrapper=self._queue_wrapper, + ) + + def register_keyboard_interrupt_handler(self): + """Register a keyboard interrupt handler which closes the COM interface and prints + a small message""" + atexit.register(keyboard_interrupt_handler, self) + + @property + def com_if_id(self): + return self._com_if.get_id() + + @property + def com_if(self) -> ComInterface: + return self._com_if + + @property + def state(self): + return self._state + + @property + def request(self): + return self._state.request + + @property + def tc_mode(self): + return self._state.mode_wrapper.tc_mode + + @property + def tm_mode(self): + return self._state.mode_wrapper.tm_mode + + @property + def inter_cmd_delay(self): + return self._queue_wrapper.inter_cmd_delay + + @inter_cmd_delay.setter + def inter_cmd_delay(self, delay: timedelta): + self._queue_wrapper.inter_cmd_delay = delay + + @tc_mode.setter + def tc_mode(self, tc_mode: TcMode): + self._state.mode_wrapper.tc_mode = tc_mode + + @tm_mode.setter + def tm_mode(self, tm_mode: TmMode): + self._state.mode_wrapper.tm_mode = tm_mode + + @property + def tm_listener(self): + return self._tm_listener + + def try_set_com_if(self, com_if: ComInterface) -> bool: + if not self.com_if_active(): + self._com_if = com_if + return True + else: + return False + + def com_if_active(self): + return self._com_if_active + + @property + def current_procedure(self) -> ProcedureHelper: + return ProcedureHelper(self._queue_wrapper.info) + + @current_procedure.setter + def current_procedure(self, proc_info: TcProcedureBase): + self._queue_wrapper.info = proc_info + + def start(self): + self.open_com_if() + + def __listener_io_error_handler(self, ctx: str): + LOGGER.error(f"Communication Interface could not be {ctx}") + LOGGER.info("TM listener will not be started") + if self.exit_on_com_if_init_failure: + LOGGER.error("Closing TMTC commander..") + self._com_if.close() + sys.exit(1) + + def open_com_if(self): + try: + self._com_if.open() + except IOError: + self.__listener_io_error_handler("opened") + self._com_if_active = True + + def close_com_if(self): + """Closes the TM listener and the communication interface + :return: + """ + try: + self._com_if.close() + except IOError: + self.__listener_io_error_handler("close") + self._com_if_active = False + + def periodic_op(self, _args: Optional[any] = None) -> BackendState: + """Periodic operation. Simply calls the :py:meth:`default_operation` function. + :raises KeyboardInterrupt: Yields info output and then propagates the exception + :raises IOError: Yields informative output and propagates exception + :""" + self.default_operation() + return self._state + + def default_operation(self): + """Command handling. This is a convenience function to call the TM and the TC operation + and then auto-determine the internal mode with the :py:meth:`mode_to_req` method. + + :raises NoValidProcedureSet: No valid procedure set to be passed to the feed callback of + the TC handler + """ + self.tm_operation() + self.tc_operation() + self.mode_to_req() + + def mode_to_req(self): + """This function will convert the internal state of the backend to a backend + :py:attr:`request`, which can be used to determine the next operation. These requests can + be treated like recommendations. + For example, for if both the TC and the TM mode are IDLE, the request will be set to + :py:attr:`BackendRequest.DELAY_IDLE` field. + """ + if self.tc_mode == TcMode.IDLE and self.tm_mode == TmMode.IDLE: + self._state._req = BackendRequest.DELAY_IDLE + elif self.tm_mode == TmMode.LISTENER and self.tc_mode == TcMode.IDLE: + self._state._req = BackendRequest.DELAY_LISTENER + elif self._seq_handler.mode == SenderMode.DONE: + if self._state.tc_mode == TcMode.ONE_QUEUE: + self.tc_mode = TcMode.IDLE + self._state._req = BackendRequest.TERMINATION_NO_ERROR + elif self._state.tc_mode == TcMode.MULTI_QUEUE: + if not self.keep_multi_queue_mode: + self._state.mode_wrapper.tc_mode = TcMode.IDLE + self._state._req = BackendRequest.CALL_NEXT + else: + if self._state.sender_res.longest_rem_delay.total_seconds() * 1000 > 0: + self._state._recommended_delay = ( + self._state.sender_res.longest_rem_delay + ) + self._state._req = BackendRequest.DELAY_CUSTOM + else: + self._state._req = BackendRequest.CALL_NEXT + + def poll_tm(self): + """Poll TM, irrespective of current TM mode""" + self._tm_listener.operation(self._com_if) + + def tm_operation(self): + """This function will fetch and forward TM data from the current communication interface + to the user TM handler. It only does so if the :py:attr:`tm_mode` is set to the LISTENER + mode + """ + if self._state.tm_mode == TmMode.LISTENER: + self._tm_listener.operation(self._com_if) + + def tc_operation(self): + """This function will handle consuming the current TC queue + if one is available, or attempting to fetch a new one if it is not. This function will only + do something if the :py:attr:`tc_mode` is set to a non IDLE value. + + It is necessary to set a valid procedure before calling this by using the + :py:attr:`current_proc_info` setter function. + + :raises NoValidProcedureSet: No valid procedure set to be passed to the feed callback of + the TC handler + """ + if self._state.tc_mode != TcMode.IDLE: + self.__check_and_execute_queue() + + def __check_and_execute_queue(self): + if self._seq_handler.mode == SenderMode.DONE: + queue = self.__prepare_tc_queue() + if queue is None: + return + LOGGER.info("Loading TC queue") + self._seq_handler.queue_wrapper = queue + self._seq_handler.resume() + self._state._sender_res = self._seq_handler.operation(self._com_if) + + def __prepare_tc_queue(self, auto_dispatch: bool = True) -> Optional[QueueWrapper]: + feed_wrapper = FeedWrapper(self._queue_wrapper, auto_dispatch) + if self._queue_wrapper.info is None: + raise NoValidProcedureSet( + "No procedure was set to pass to the feed callback function" + ) + self._tc_handler.feed_cb( + ProcedureHelper(self._queue_wrapper.info), feed_wrapper + ) + if not feed_wrapper.dispatch_next_queue: + return None + return feed_wrapper.queue_helper.queue_wrapper diff --git a/src/tmtccmd/core/globals_manager.py b/tmtccmd/core/globals_manager.py similarity index 100% rename from src/tmtccmd/core/globals_manager.py rename to tmtccmd/core/globals_manager.py diff --git a/src/tmtccmd/fsfw/__init__.py b/tmtccmd/fsfw/__init__.py similarity index 100% rename from src/tmtccmd/fsfw/__init__.py rename to tmtccmd/fsfw/__init__.py diff --git a/tmtccmd/gui/__init__.py b/tmtccmd/gui/__init__.py new file mode 100644 index 00000000..1e2e8c9e --- /dev/null +++ b/tmtccmd/gui/__init__.py @@ -0,0 +1 @@ +from .frontend import TmTcFrontend diff --git a/tmtccmd/gui/buttons.py b/tmtccmd/gui/buttons.py new file mode 100644 index 00000000..515ef42c --- /dev/null +++ b/tmtccmd/gui/buttons.py @@ -0,0 +1,195 @@ +from typing import Callable, Optional + +from PyQt5.QtCore import QThreadPool, QRunnable +from PyQt5.QtWidgets import QPushButton + +from tmtccmd import TmTcCfgHookBase, get_console_logger, DefaultProcedureInfo +from tmtccmd.gui.defs import ( + SharedArgs, + LocalArgs, + WorkerOperationsCodes, + DISCONNECT_BTTN_STYLE, + CONNECT_BTTN_STYLE, + COMMAND_BUTTON_STYLE, +) +from tmtccmd.gui.defs import FrontendState +from tmtccmd.gui.worker import FrontendWorker + +LOGGER = get_console_logger() + + +class ButtonArgs: + def __init__( + self, + state: FrontendState, + pool: QThreadPool, + shared: SharedArgs, + ): + self.state = state + self.pool = pool + self.shared = shared + + +class ConnectButtonParams: + def __init__( + self, + hook_obj: TmTcCfgHookBase, + connect_cb: Callable[[], None], + disconnect_cb: Callable[[], None], + tm_listener_bttn: Optional[QPushButton], + ): + self.hook_obj = hook_obj + self.connect_cb = connect_cb + self.disconnect_cb = disconnect_cb + self.tm_listener_bttn = tm_listener_bttn + + +class ConnectButtonWrapper: + def __init__( + self, button: QPushButton, args: ButtonArgs, bttn_params: ConnectButtonParams + ): + self.button = button + self._args = args + self._bttn_params = bttn_params + self._connected = False + self._next_con_state = False + self.button.clicked.connect(self._button_op) + + def _button_op(self): + if not self._connected: + self._connect_button_pressed() + else: + self._disconnect_button_pressed() + + def _connect_button_pressed(self): + LOGGER.info("Opening COM Interface") + # Build and assign new communication interface + if self._args.state.current_com_if != self._args.state.last_com_if: + LOGGER.info("Switching COM Interface") + new_com_if = self._bttn_params.hook_obj.assign_communication_interface( + com_if_key=self._args.state.current_com_if + ) + self._args.state.last_com_if = self._args.state.current_com_if + set_success = self._args.shared.backend.try_set_com_if(new_com_if) + if not set_success: + LOGGER.warning( + f"Could not set new communication interface {new_com_if}" + ) + self.button.setEnabled(False) + worker = FrontendWorker( + LocalArgs(WorkerOperationsCodes.OPEN_COM_IF, None), self._args.shared + ) + self._next_con_state = True + worker.signals.finished.connect(self._button_op_done) + # TODO: Connect failure signal as well + self._args.pool.start(worker) + + def _button_op_done(self): + if self._next_con_state: + self._connect_button_finished() + else: + self._disconnect_button_finished() + self._connected = self._next_con_state + + def _connect_button_finished(self): + self.button.setStyleSheet(DISCONNECT_BTTN_STYLE) + self.button.setText("Disconnect") + self.button.setEnabled(True) + self._bttn_params.connect_cb() + if ( + self._args.state.auto_connect_tm_listener + and self._bttn_params.tm_listener_bttn is not None + ): + self._bttn_params.tm_listener_bttn.click() + LOGGER.info("Connected") + + def _disconnect_button_pressed(self): + self.button.setEnabled(False) + self._next_con_state = False + worker = FrontendWorker( + LocalArgs(WorkerOperationsCodes.CLOSE_COM_IF, None), self._args.shared + ) + worker.signals.finished.connect(self._button_op_done) + self._args.pool.start(worker) + + def _disconnect_button_finished(self): + self.button.setEnabled(True) + self.button.setStyleSheet(CONNECT_BTTN_STYLE) + self.button.setText("Connect") + self._bttn_params.disconnect_cb() + LOGGER.info("Disconnected") + + +class TmButtonWrapper: + def __init__(self, button: QPushButton, args: ButtonArgs, conn_button: QPushButton): + self.button = button + self.args = args + self.worker: Optional[QRunnable] = None + self._listening = False + self._next_listener_state = False + self.button.setStyleSheet(CONNECT_BTTN_STYLE) + self.button.setText("Start TM listener") + self.button.setEnabled(False) + self.button.clicked.connect(self.button_op) + self._conn_button = conn_button + + def button_op(self): + if not self._listening: + LOGGER.info("Starting TM listener") + self.worker = FrontendWorker( + LocalArgs(WorkerOperationsCodes.LISTEN_FOR_TM, 0.4), self.args.shared + ) + self._next_listener_state = True + self._conn_button.setDisabled(True) + self.args.pool.start(self.worker) + self.button_op_done() + else: + LOGGER.info("Stopping TM listener") + self._next_listener_state = False + self.worker.signals.finished.connect(self.button_op_done) + self.worker.signals.stop.emit(None) + self.button.setEnabled(False) + + def button_op_done(self): + if self._next_listener_state: + self.button.setStyleSheet(DISCONNECT_BTTN_STYLE) + self.button.setText("Stop TM listener") + self._listening = True + self.button.setEnabled(True) + else: + self.button.setStyleSheet(CONNECT_BTTN_STYLE) + if not self.args.shared.com_if_ref_tracker.is_used(): + self._conn_button.setEnabled(True) + self.button.setText("Start TM listener") + self._listening = False + self.button.setEnabled(True) + + +class SendButtonWrapper: + def __init__(self, button: QPushButton, args: ButtonArgs, conn_button: QPushButton): + self.button = button + self._args = args + self._conn_button = conn_button + self.debug_mode = False + self.button.setText("Send Command") + self.button.setStyleSheet(COMMAND_BUTTON_STYLE) + self.button.setEnabled(False) + self.button.clicked.connect(self._button_op) + + def _button_op(self): + if self.debug_mode: + LOGGER.info("Send command button pressed.") + self.button.setDisabled(True) + self._args.shared.backend.current_procedure = DefaultProcedureInfo( + self._args.state.current_service, self._args.state.current_op_code + ) + worker = FrontendWorker( + LocalArgs(WorkerOperationsCodes.ONE_QUEUE_MODE, None), self._args.shared + ) + worker.signals.finished.connect(self._finish_op) + self._args.pool.start(worker) + + def _finish_op(self): + self.button.setEnabled(True) + if not self._args.shared.com_if_ref_tracker.is_used(): + self._conn_button.setEnabled(True) diff --git a/tmtccmd/gui/defs.py b/tmtccmd/gui/defs.py new file mode 100644 index 00000000..f87c2549 --- /dev/null +++ b/tmtccmd/gui/defs.py @@ -0,0 +1,91 @@ +import enum +import threading + +from tmtccmd import CcsdsTmtcBackend +from tmtccmd.config import CoreComInterfaces +from tmtccmd.core import BackendController + +CONNECT_BTTN_STYLE = ( + "background-color: #1fc600;" + "border-style: inset;" + "font: bold;" + "padding: 6px;" + "border-width: 2px;" + "border-radius: 6px;" +) + + +DISCONNECT_BTTN_STYLE = ( + "background-color: orange;" + "border-style: inset;" + "font: bold;" + "padding: 6px;" + "border-width: 2px;" + "border-radius: 6px;" +) + + +COMMAND_BUTTON_STYLE = ( + "background-color: #cdeefd;" + "border-style: inset;" + "font: bold;" + "padding: 6px;" + "border-width: 2px;" + "border-radius: 6px;" +) + + +class WorkerOperationsCodes(enum.IntEnum): + OPEN_COM_IF = 0 + CLOSE_COM_IF = 1 + ONE_QUEUE_MODE = 2 + LISTEN_FOR_TM = 3 + UPDATE_BACKEND_MODE = 4 + IDLE = 5 + + +class ComIfRefCount: + def __init__(self): + self.lock = threading.Lock() + self.com_if_used = False + self.user_cnt = 0 + + def add_user(self): + with self.lock: + self.user_cnt += 1 + + def remove_user(self): + with self.lock: + if self.user_cnt > 0: + self.user_cnt -= 1 + + def is_used(self): + with self.lock: + if self.user_cnt > 0: + return True + return False + + +class LocalArgs: + def __init__(self, op_code: WorkerOperationsCodes, op_code_args: any = None): + self.op_code = op_code + self.op_args = op_code_args + + +class SharedArgs: + def __init__(self, backend: CcsdsTmtcBackend): + self.ctrl = BackendController() + self.state_lock = threading.Lock() + self.com_if_ref_tracker = ComIfRefCount() + self.tc_lock = threading.Lock() + self.backend = backend + + +class FrontendState: + def __init__(self): + self.current_com_if = CoreComInterfaces.UNSPECIFIED.value + self.current_service = "" + self.current_op_code = "" + self.auto_connect_tm_listener = True + self.last_com_if = CoreComInterfaces.UNSPECIFIED.value + self.current_com_if_key = CoreComInterfaces.UNSPECIFIED.value diff --git a/tmtccmd/gui/frontend.py b/tmtccmd/gui/frontend.py new file mode 100644 index 00000000..75f89db2 --- /dev/null +++ b/tmtccmd/gui/frontend.py @@ -0,0 +1,417 @@ +"""PyQt front end components for the tmtccmd framework. +@author R. Mueller, P. Scheurenbrand, D. Nguyen +""" +import os +import sys +import webbrowser +from multiprocessing import Process +from pathlib import Path +from typing import Union + +from PyQt5.QtWidgets import ( + QMainWindow, + QGridLayout, + QTableWidget, + QWidget, + QLabel, + QCheckBox, + QDoubleSpinBox, + QFrame, + QComboBox, + QPushButton, + QTableWidgetItem, + QMenu, + QAction, +) +from PyQt5.QtGui import QPixmap, QIcon, QFont +from PyQt5.QtCore import ( + Qt, + QThreadPool, +) + +from tmtccmd.core.base import FrontendBase +from tmtccmd.config.globals import CoreGlobalIds +from tmtccmd.core.ccsds_backend import CcsdsTmtcBackend +from tmtccmd.config import TmTcCfgHookBase +from tmtccmd.gui.buttons import ( + ConnectButtonParams, + ButtonArgs, + SendButtonWrapper, + TmButtonWrapper, + ConnectButtonWrapper, +) +from tmtccmd.gui.defs import SharedArgs, CONNECT_BTTN_STYLE, FrontendState +from tmtccmd.logging import get_console_logger +from tmtccmd.core.globals_manager import get_global, update_global +from tmtccmd.com_if.tcpip_utils import TcpIpConfigIds +import tmtccmd as mod_root + +LOGGER = get_console_logger() + + +class TmTcFrontend(QMainWindow, FrontendBase): + def __init__( + self, hook_obj: TmTcCfgHookBase, tmtc_backend: CcsdsTmtcBackend, app_name: str + ): + super(TmTcFrontend, self).__init__() + super(QMainWindow, self).__init__() + self._app_name = app_name + self._shared_args = SharedArgs(tmtc_backend) + tmtc_backend.exit_on_com_if_init_failure = False + self._hook_obj = hook_obj + self._service_list = [] + self._op_code_list = [] + self._com_if_list = [] + self._service_op_code_dict = hook_obj.get_tmtc_definitions() + self._state = FrontendState() + self._thread_pool = QThreadPool() + self.__connected = False + self.__debug_mode = True + + self.__combo_box_op_codes: Union[None, QComboBox] = None + self.logo_path = Path( + f"{Path(mod_root.__file__).parent.parent}/misc/logo-tiny.png" + ) + + def prepare_start(self, args: any) -> Process: + return Process(target=self.start) + + def start(self, qt_app: any): + self.__start_ui() + sys.exit(qt_app.exec()) + + def set_gui_logo(self, logo_total_path: str): + if os.path.isfile(logo_total_path): + self.logo_path = logo_total_path + else: + LOGGER.warning("Could not set logo, path invalid!") + + def __start_ui(self): + self.__create_menu_bar() + win = QWidget(self) + self.setCentralWidget(win) + + grid = QGridLayout() + win.setLayout(grid) + row = 0 + self.setWindowTitle(self._app_name) + print(self.logo_path) + self.setWindowIcon(QIcon(self.logo_path.as_posix())) + + add_pixmap = False + + if add_pixmap: + row = self.__set_up_pixmap(grid=grid, row=row) + + row = self.__set_up_config_section(grid=grid, row=row) + row = self.__add_vertical_separator(grid=grid, row=row) + + tm_listener_button = QPushButton() + conn_bttn_params = ConnectButtonParams( + hook_obj=self._hook_obj, + connect_cb=self.__connected_com_if_cb, + disconnect_cb=self.__disconnect_com_if_cb, + tm_listener_bttn=tm_listener_button, + ) + # com if configuration + row, self.__connect_button_wrapper = self.__set_up_com_if_section( + conn_bttn_params=conn_bttn_params, grid=grid, row=row + ) + row = self.__add_vertical_separator(grid=grid, row=row) + + tmtc_ctrl_label = QLabel("TMTC Control") + font = QFont() + font.setBold(True) + tmtc_ctrl_label.setFont(font) + grid.addWidget(tmtc_ctrl_label, row, 0, 1, 2) + row += 1 + row = self.__set_up_service_op_code_section(grid=grid, row=row) + + button_args = ButtonArgs( + state=self._state, pool=self._thread_pool, shared=self._shared_args + ) + self.__send_bttn_wrapper = SendButtonWrapper( + button=QPushButton(), + args=button_args, + conn_button=self.__connect_button_wrapper.button, + ) + grid.addWidget(self.__send_bttn_wrapper.button, row, 0, 1, 2) + row += 1 + + self.__tm_button_wrapper = TmButtonWrapper( + button=tm_listener_button, + args=button_args, + conn_button=self.__connect_button_wrapper.button, + ) + grid.addWidget(self.__tm_button_wrapper.button, row, 0, 1, 2) + row += 1 + self.show() + + def __create_menu_bar(self): + menu_bar = self.menuBar() + # Creating menus using a QMenu object + file_menu = QMenu("&File", self) + menu_bar.addMenu(file_menu) + # Creating menus using a title + help_menu = menu_bar.addMenu("&Help") + + help_action = QAction("Help", self) + help_action.triggered.connect(self.__help_url) + help_menu.addAction(help_action) + + @staticmethod + def __help_url(): + webbrowser.open("https://tmtccmd.readthedocs.io/en/latest/") + + def __set_up_config_section(self, grid: QGridLayout, row: int) -> int: + label = QLabel("Configuration") + font = QFont() + font.setBold(True) + label.setFont(font) + grid.addWidget(label, row, 0, 1, 2) + row += 1 + + start_listener_on_connect = QCheckBox("Auto-Connect TM listener") + start_listener_on_connect.setChecked(True) + start_listener_on_connect.stateChanged.connect( + lambda: self._tm_auto_connect_changed(start_listener_on_connect) + ) + grid.addWidget(start_listener_on_connect, row, 0, 1, 1) + row += 1 + + grid.addWidget(QLabel("Inter-Packet Delay Seconds [0 - 500]"), row, 0, 1, 2) + row += 1 + + spin_timeout = QDoubleSpinBox() + spin_timeout.setValue(0.1) + # TODO: set sensible min/max values + spin_timeout.setSingleStep(0.1) + spin_timeout.setMinimum(0.0) + spin_timeout.setMaximum(500.0) + # https://youtrack.jetbrains.com/issue/PY-22908 + # Ignore those warnings for now. + spin_timeout.valueChanged.connect(number_timeout) + grid.addWidget(spin_timeout, row, 0, 1, 1) + row += 1 + return row + + def _tm_auto_connect_changed(self, box: QCheckBox): + if box.isChecked(): + self._state.auto_connect_tm_listener = True + else: + self._state.auto_connect_tm_listener = False + + def __set_up_com_if_section( + self, conn_bttn_params: ConnectButtonParams, grid: QGridLayout, row: int + ) -> (int, ConnectButtonWrapper): + font = QFont() + font.setBold(True) + label = QLabel("Communication Interface") + label.setFont(font) + grid.addWidget(label, row, 0, 1, 1) + com_if_combo_box = QComboBox() + all_com_ifs = self._hook_obj.get_com_if_dict() + index = 0 + # add all possible ComIFs to the comboBox + for id, com_if_value in all_com_ifs.items(): + com_if_combo_box.addItem(com_if_value[0]) + self._com_if_list.append((id, com_if_value[0])) + if self._shared_args.backend.com_if_id == id: + com_if_combo_box.setCurrentIndex(index) + index += 1 + com_if_combo_box.currentIndexChanged.connect(self.__com_if_sel_index_changed) + grid.addWidget(com_if_combo_box, row, 1, 1, 1) + row += 1 + + self.com_if_cfg_button = QPushButton() + self.com_if_cfg_button.setText("Configure") + grid.addWidget(self.com_if_cfg_button, row, 0, 1, 2) + row += 1 + + connect_button = QPushButton() + connect_button.setText("Connect") + connect_button.setStyleSheet(CONNECT_BTTN_STYLE) + conn_bttn_wrapper = ConnectButtonWrapper( + button=connect_button, + args=ButtonArgs(self._state, self._thread_pool, self._shared_args), + bttn_params=conn_bttn_params, + ) + grid.addWidget(connect_button, row, 0, 1, 2) + row += 1 + return row, conn_bttn_wrapper + + def __disable_conn_bttn(self): + self.__connect_button_wrapper.button.setDisabled(True) + + def __enable_conn_bttn(self): + self.__connect_button_wrapper.button.setEnabled(True) + + def __connected_com_if_cb(self): + self.__send_bttn_wrapper.button.setEnabled(True) + self.__tm_button_wrapper.button.setEnabled(True) + + def __disconnect_com_if_cb(self): + self.__send_bttn_wrapper.button.setDisabled(True) + self.__tm_button_wrapper.button.setDisabled(True) + + def __set_up_service_op_code_section(self, grid: QGridLayout, row: int): + grid.addWidget(QLabel("Service: "), row, 0, 1, 2) + grid.addWidget(QLabel("Operation Code: "), row, 1, 1, 2) + row += 1 + + combo_box_services = QComboBox() + default_service = get_global(CoreGlobalIds.CURRENT_SERVICE) + self._service_op_code_dict = self._hook_obj.get_tmtc_definitions() + if self._service_op_code_dict is None: + LOGGER.warning("Invalid service to operation code dictionary") + LOGGER.warning("Setting default dictionary") + from tmtccmd.config.globals import get_default_tmtc_defs + + self._service_op_code_dict = get_default_tmtc_defs() + index = 0 + default_index = 0 + for service_key, service_value in self._service_op_code_dict.defs.items(): + combo_box_services.addItem(service_value[0]) + if service_key == default_service: + default_index = index + self._service_list.append(service_key) + index += 1 + combo_box_services.setCurrentIndex(default_index) + self._state.current_service = self._service_list[default_index] + + combo_box_services.currentIndexChanged.connect(self.__service_index_changed) + grid.addWidget(combo_box_services, row, 0, 1, 1) + + self.__combo_box_op_codes = QComboBox() + self._state.current_service = self._service_list[default_index] + self.__update_op_code_combo_box() + self.__combo_box_op_codes.currentIndexChanged.connect( + self.__op_code_index_changed + ) + # TODO: Combo box also needs to be updated if another service is selected + grid.addWidget(self.__combo_box_op_codes, row, 1, 1, 1) + row += 1 + return row + + def __set_up_pixmap(self, grid: QGridLayout, row: int) -> int: + label = QLabel(self) + label.setGeometry(720, 10, 100, 100) + label.adjustSize() + + pixmap = QPixmap(self.logo_path) + pixmap_width = pixmap.width() + pixmap_height = pixmap.height() + row += 1 + + pixmap_scaled = pixmap.scaled( + pixmap_width * 0.3, pixmap_height * 0.3, Qt.KeepAspectRatio + ) + label.setPixmap(pixmap_scaled) + label.setScaledContents(True) + + grid.addWidget(label, row, 0, 1, 2) + row += 1 + return row + + @staticmethod + def __add_vertical_separator(grid: QGridLayout, row: int): + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + grid.addWidget(separator, row, 0, 1, 2) + row += 1 + return row + + def __service_index_changed(self, index: int): + self._current_service = self._service_list[index] + self.__update_op_code_combo_box() + if self.__debug_mode: + LOGGER.info("Service changed") + + def __op_code_index_changed(self, index: int): + self._state.current_op_code = self._op_code_list[index] + if self.__debug_mode: + LOGGER.info("Op Code changed") + + def __update_op_code_combo_box(self): + self.__combo_box_op_codes.clear() + self._op_code_list = [] + op_code_entry = self._service_op_code_dict.op_code_entry( + self._state.current_service + ) + if op_code_entry is not None: + for op_code_key, op_code_value in op_code_entry.op_code_dict.items(): + try: + self._op_code_list.append(op_code_key) + self.__combo_box_op_codes.addItem(op_code_value[0]) + except TypeError: + LOGGER.warning(f"Invalid op code entry {op_code_value}, skipping..") + self._state.current_op_code = self._op_code_list[0] + + def __checkbox_log_update(self, state: int): + update_global(CoreGlobalIds.PRINT_TO_FILE, state) + if self.__debug_mode: + LOGGER.info(["Enabled", "Disabled"][state == 0] + " print to log.") + + def __checkbox_console_update(self, state: bool): + update_global(CoreGlobalIds.PRINT_TM, state) + if self.__debug_mode: + LOGGER.info(["enabled", "disabled"][state == 0] + " console print") + + def __checkbox_print_raw_data_update(self, state: int): + update_global(CoreGlobalIds.PRINT_RAW_TM, state) + if self.__debug_mode: + LOGGER.info(["enabled", "disabled"][state == 0] + " printing of raw data") + + def __com_if_sel_index_changed(self, index: int): + self._state.current_com_if = self._com_if_list[index][0] + if self.__debug_mode: + LOGGER.info(f"Communication IF updated: {self._com_if_list[index][1]}") + + +class SingleCommandTable(QTableWidget): + def __init__(self): + super().__init__() + self.setRowCount(1) + self.setColumnCount(5) + self.setHorizontalHeaderItem(0, QTableWidgetItem("Service")) + self.setHorizontalHeaderItem(1, QTableWidgetItem("Subservice")) + self.setHorizontalHeaderItem(2, QTableWidgetItem("SSC")) + self.setHorizontalHeaderItem(3, QTableWidgetItem("Data")) + self.setHorizontalHeaderItem(4, QTableWidgetItem("CRC")) + self.setItem(0, 0, QTableWidgetItem("17")) + self.setItem(0, 1, QTableWidgetItem("1")) + self.setItem(0, 2, QTableWidgetItem("20")) + + +def checkbox_print_hk_data(state: int): + update_global(CoreGlobalIds.PRINT_HK, state) + LOGGER.info(["enabled", "disabled"][state == 0] + " printing of hk data") + + +def checkbox_short_display_mode(state: int): + update_global(CoreGlobalIds.DISPLAY_MODE, state) + LOGGER.info(["enabled", "disabled"][state == 0] + " short display mode") + + +def number_timeout(value: float): + update_global(CoreGlobalIds.TM_TIMEOUT, value) + LOGGER.info("PUS TM timeout changed to: " + str(value)) + + +def number_timeout_factor(value: float): + update_global(CoreGlobalIds.TC_SEND_TIMEOUT_FACTOR, value) + LOGGER.info("PUS TM timeout factor changed to: " + str(value)) + + +def ip_change_client(value): + ethernet_config = get_global(CoreGlobalIds.ETHERNET_CONFIG) + ethernet_config[TcpIpConfigIds.RECV_ADDRESS] = value + update_global(CoreGlobalIds.ETHERNET_CONFIG, ethernet_config) + LOGGER.info("Client IP changed: " + value) + + +def ip_change_board(value): + ethernet_config = get_global(CoreGlobalIds.ETHERNET_CONFIG) + ethernet_config[TcpIpConfigIds.SEND_ADDRESS] = value + update_global(CoreGlobalIds.ETHERNET_CONFIG, ethernet_config) + LOGGER.info("Board IP changed: " + value) diff --git a/tmtccmd/gui/worker.py b/tmtccmd/gui/worker.py new file mode 100644 index 00000000..bd00a79e --- /dev/null +++ b/tmtccmd/gui/worker.py @@ -0,0 +1,126 @@ +import time + +from PyQt5.QtCore import QRunnable, pyqtSlot, QObject, pyqtSignal + +from tmtccmd import get_console_logger +from tmtccmd.core import TmMode, TcMode, BackendRequest +from tmtccmd.gui.defs import LocalArgs, SharedArgs, WorkerOperationsCodes + + +class WorkerSignalWrapper(QObject): + finished = pyqtSignal(object) + failure = pyqtSignal(object) + stop = pyqtSignal(object) + + +class FrontendWorker(QRunnable): + """Runnable thread which can be used with QThreadPool. Not used for now, might be needed + in the future. + """ + + def __init__(self, local_args: LocalArgs, shared_args: SharedArgs): + super(QRunnable, self).__init__() + self._locals = local_args + self._shared = shared_args + self.signals = WorkerSignalWrapper() + self._stop_signal = False + self.signals.stop.connect(self._stop_com_if) + + def __sanitize_locals(self): + if self._locals.op_code == WorkerOperationsCodes.LISTEN_FOR_TM: + if self._locals.op_args is None or not isinstance( + float, self._locals.op_args + ): + self._locals.op_args = 0.2 + + def __setup(self, op_code: WorkerOperationsCodes) -> bool: + if op_code == WorkerOperationsCodes.OPEN_COM_IF: + if self._shared.backend.com_if_active(): + self._finish_with_info("COM Interface is already active") + else: + self._shared.backend.open_com_if() + self._finish_success() + return False + if op_code == WorkerOperationsCodes.CLOSE_COM_IF: + if not self._shared.backend.com_if_active(): + self._finish_with_info("COM Interface is not active") + elif self._shared.com_if_ref_tracker.is_used(): + self._failure_with_info("Can not close COM interface which is used") + else: + self._shared.backend.close_com_if() + self._finish_success() + return False + if op_code == WorkerOperationsCodes.ONE_QUEUE_MODE: + self._shared.com_if_ref_tracker.add_user() + with self._shared.tc_lock: + self._shared.backend.tc_mode = TcMode.ONE_QUEUE + elif op_code == WorkerOperationsCodes.LISTEN_FOR_TM: + self._shared.com_if_ref_tracker.add_user() + self._shared.backend.tm_mode = TmMode.LISTENER + return True + + def __loop(self, op_code: WorkerOperationsCodes) -> bool: + if op_code == WorkerOperationsCodes.ONE_QUEUE_MODE: + self._shared.tc_lock.acquire() + self._shared.backend.tc_operation() + self._update_backend_mode() + state = self._shared.backend.state + if state.request == BackendRequest.TERMINATION_NO_ERROR: + self._shared.tc_lock.release() + self._shared.com_if_ref_tracker.remove_user() + with self._shared.state_lock: + if ( + not self._shared.com_if_ref_tracker.is_used() + and self._locals.op_args is not None + ): + self._locals.op_args() + self._finish_success() + return False + elif state.request == BackendRequest.DELAY_CUSTOM: + self._shared.tc_lock.release() + time.sleep(state.next_delay) + elif state.request == BackendRequest.CALL_NEXT: + self._shared.tc_lock.release() + elif op_code == WorkerOperationsCodes.LISTEN_FOR_TM: + if not self._stop_signal: + # We only should run the TM operation here + self._shared.backend.tm_operation() + # Poll TM every 400 ms for now + time.sleep(self._locals.op_args) + else: + self._shared.com_if_ref_tracker.remove_user() + self._finish_success() + return False + elif op_code == WorkerOperationsCodes.IDLE: + return False + else: + # This must be a programming error + get_console_logger().error( + f"Unknown worker operation code {self._locals.op_code}" + ) + return True + + @pyqtSlot() + def run(self): + op_code = self._locals.op_code + loop_required = self.__setup(op_code) + if loop_required: + while True: + if not self.__loop(op_code): + break + + def _finish_success(self): + self.signals.finished.emit(None) + + def _finish_with_info(self, info: str): + self.signals.finished.emit(info) + + def _failure_with_info(self, info: str): + self.signals.failure.emit(info) + + def _update_backend_mode(self): + with self._shared.state_lock: + self._shared.backend.mode_to_req() + + def _stop_com_if(self, _args: any): + self._stop_signal = True diff --git a/src/tmtccmd/logging/__init__.py b/tmtccmd/logging/__init__.py similarity index 100% rename from src/tmtccmd/logging/__init__.py rename to tmtccmd/logging/__init__.py diff --git a/tmtccmd/logging/pus.py b/tmtccmd/logging/pus.py new file mode 100644 index 00000000..a4623038 --- /dev/null +++ b/tmtccmd/logging/pus.py @@ -0,0 +1,177 @@ +from __future__ import annotations +import enum +import logging +from pathlib import Path +from typing import Optional, Union +from datetime import datetime + +from spacepackets.ecss import PusTelecommand, PusTelemetry +from tmtccmd.logging import LOG_DIR +from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler +from logging import FileHandler + +RAW_PUS_LOGGER_NAME = "pus-log" +RAW_PUS_FILE_BASE_NAME = RAW_PUS_LOGGER_NAME + +TMTC_LOGGER_NAME = "tmtc-log" +TMTC_FILE_BASE_NAME = TMTC_LOGGER_NAME + +__TMTC_LOGGER: Optional[logging.Logger] = None +__RAW_PUS_LOGGER: Optional[logging.Logger] = None + + +def date_suffix() -> str: + return f"{datetime.now().date()}" + + +class TimedLogWhen(enum.Enum): + PER_HOUR = "H" + PER_MINUTE = "M" + PER_SECOND = "S" + PER_DAY = "D" + + +class RawTmtcLogBase: + def __init__( + self, logger: logging.Logger, log_repr: bool = True, log_raw_repr: bool = True + ): + self.logger = logger + self.do_log_repr = log_repr + self.do_log_raw_repr = log_raw_repr + self.counter = 0 + + def log_tc(self, packet: PusTelecommand): + """Default log function which logs the Python packet representation and raw bytes""" + prefix = self.tc_prefix(packet, self.counter) + if self.do_log_repr: + self.log_repr(prefix, packet) + raw_bytes = packet.pack() + self.__log_raw_inc_counter(prefix, raw_bytes) + + def log_tm(self, packet: PusTelemetry): + """Default log function which logs the Python packet representation and raw bytes""" + prefix = self.tm_prefix(packet, self.counter) + if self.do_log_repr: + self.log_repr(prefix, packet) + raw_bytes = packet.pack() + self.__log_raw_inc_counter(prefix, raw_bytes) + + def __log_raw_inc_counter(self, prefix: str, raw: bytes): + self.log_bytes_readable(prefix, raw) + if self.do_log_raw_repr: + self.log_bytes_repr(prefix, raw) + self.counter += 1 + + def log_repr(self, prefix: str, packet: Union[PusTelecommand, PusTelemetry]): + self.logger.info(f"{prefix} repr: {packet!r}") + + @staticmethod + def tc_prefix(packet: PusTelecommand, counter: int): + return f"tc {counter} [{packet.service}, {packet.subservice}]" + + @staticmethod + def tm_prefix(packet: PusTelemetry, counter: int): + return f"tm {counter} [{packet.service}, {packet.subservice}]" + + def log_bytes_readable(self, prefix: str, packet: bytes): + self.logger.info(f"{prefix} raw readable hex: [{packet.hex(sep=',')}]") + + def log_bytes_repr(self, prefix: str, packet: bytes): + self.logger.info(f"{prefix} raw repr: {packet!r}") + + +class RawTmtcTimedLogWrapper(RawTmtcLogBase): + def __init__( + self, + when: TimedLogWhen, + interval: int, + file_name: Path = Path(f"{LOG_DIR}/{RAW_PUS_FILE_BASE_NAME}.log"), + ): + """Create a raw TMTC timed rotating log wrapper. + See the official Python documentation at + https://docs.python.org/3/library/logging.handlers.html#timedrotatingfilehandler for + more information on the input parameters + + :param when: A new log file will be created at the product of when and interval + :param interval: A new log file will be created at the product of when and interval. + For example, using when="H" and interval=3, a new log file will be created in three + hour intervals + :param file_name: Base filename of the log file + """ + logger = logging.getLogger(RAW_PUS_LOGGER_NAME) + formatter = logging.Formatter( + fmt="%(asctime)s.%(msecs)03d: %(message)s", datefmt="%Y-%m-%d %H:%M:%S" + ) + handler = TimedRotatingFileHandler( + filename=file_name, when=when.value, interval=interval + ) + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + self.file_name = handler.baseFilename + super().__init__(logger) + + +class RawTmtcRotatingLogWrapper(RawTmtcLogBase): + def __init__( + self, + max_bytes: int, + backup_count: int, + file_name: Path = Path(f"{LOG_DIR}/{RAW_PUS_FILE_BASE_NAME}"), + suffix: str = date_suffix(), + ): + """Create a raw TMTC rotating log wrapper. + See the official Python documentation at + https://docs.python.org/3/library/logging.handlers.html#rotatingfilehandler for + more information on the input parameters + + :param max_bytes: Maximum number of bytes per file. If backup_count is non-zero, the handler + will create a new file up to the number of back_up count files. If the maximum backup + count is reached, the oldest files will be deleted + :param backup_count: If this is zero, Only max_bytes can be stored. Otherwise, a rollover + will occur when a file reaches max_bytes and up to back_count files can be created + this way. + :param file_name: Base filename of the log file + :param suffix: Suffix of the log file. Can be used to change the used log file. The default + argument will use a date suffix, which will lead to a new unique rotating log created + every day + """ + logger = logging.getLogger(RAW_PUS_LOGGER_NAME) + formatter = logging.Formatter( + fmt="%(asctime)s.%(msecs)03d: %(message)s", datefmt="%Y-%m-%d %H:%M:%S" + ) + handler = RotatingFileHandler( + filename=f"{file_name}_{suffix}.log", + maxBytes=max_bytes, + backupCount=backup_count, + ) + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging.INFO) + self.file_name = handler.baseFilename + super().__init__(logger) + + +class RegularTmtcLogWrapper: + def __init__(self, file_name: Optional[Path] = None): + if file_name is None: + self.file_name = self.get_current_tmtc_file_name() + else: + self.file_name = file_name + self.logger = logging.getLogger(TMTC_LOGGER_NAME) + self.file_handler = FileHandler(self.file_name) + formatter = logging.Formatter() + self.file_handler.setFormatter(formatter) + self.logger.addHandler(self.file_handler) + self.logger.setLevel(logging.INFO) + + @classmethod + def get_current_tmtc_file_name(cls) -> Path: + return Path( + f"{LOG_DIR}/{TMTC_FILE_BASE_NAME}_{datetime.now().date()}_" + f"{datetime.now().time().strftime('%H%M%S')}.log" + ) + + def __del__(self): + self.logger.removeHandler(self.file_handler) + self.file_handler.close() diff --git a/tmtccmd/pus/__init__.py b/tmtccmd/pus/__init__.py new file mode 100644 index 00000000..024a9fac --- /dev/null +++ b/tmtccmd/pus/__init__.py @@ -0,0 +1,183 @@ +from enum import IntEnum +from typing import Optional + +from spacepackets.ecss import PusTelecommand +from spacepackets.ecss.defs import PusServices +from spacepackets.ecss.pus_1_verification import RequestId +import spacepackets.ecss.pus_1_verification as pus_1 +from spacepackets.ecss.pus_verificator import ( + VerificationStatus, + StatusField, + PusVerificator, + TmCheckResult, +) +from .seqcnt import FileSeqCountProvider, ProvidesSeqCount +import logging + +from tmtccmd.utility.conf_util import AnsiColors + + +class CustomPusServices(IntEnum): + SERVICE_200_MODE = 200 + + +class VerificationWrapper: + def __init__( + self, + pus_verificator: PusVerificator, + console_logger: Optional[logging.Logger], + file_logger: Optional[logging.Logger], + ): + self.pus_verificator = pus_verificator + self.console_logger = console_logger + self.file_logger = file_logger + self.with_colors = True + + @property + def verificator(self) -> PusVerificator: + return self.pus_verificator + + def add_tc(self, pus_tc: PusTelecommand) -> bool: + return self.pus_verificator.add_tc(pus_tc) + + def add_tm(self, srv_1_tm: pus_1.Service1Tm) -> TmCheckResult: + return self.pus_verificator.add_tm(srv_1_tm) + + def log_to_console(self, srv_1_tm: pus_1.Service1Tm, res: TmCheckResult): + self.log_to_console_from_req_id(srv_1_tm.tc_req_id, res, srv_1_tm.subservice) + + def log_to_console_from_req_id( + self, + req_id: RequestId, + res: TmCheckResult, + subservice: Optional[pus_1.Subservices] = None, + ): + return self.log_progress_to_console_from_status(res.status, req_id, subservice) + + def log_to_file(self, srv_1_tm: pus_1.Service1Tm, res: TmCheckResult): + self.log_to_file_from_req_id(srv_1_tm.tc_req_id, res, srv_1_tm.subservice) + + def log_to_file_from_req_id( + self, + req_id: RequestId, + res: TmCheckResult, + subservice: Optional[pus_1.Subservices] = None, + ): + self.log_to_file_from_status(res.status, req_id, subservice) + + def log_to_file_from_status( + self, + status: VerificationStatus, + req_id: RequestId, + subservice: Optional[pus_1.Subservices] = None, + ): + if self.file_logger is None: + raise ValueError("No valid file logger was set") + acc_char = gen_file_char_from_status(status.accepted) + start_char = gen_file_char_from_status(status.started) + step_char = gen_file_char_from_status(status.step) + fin_char = gen_file_char_from_status(status.completed) + step_num = self.step_num(status) + first_str = self._get_info_string(subservice) + second_str = f"Request ID {req_id.as_u32():#04x}" + completion_str = "" + if status.completed == StatusField.SUCCESS: + completion_str = " S" + third_str = ( + f"acc ({acc_char}) sta ({start_char}) ste ({step_char}, {step_num}) " + f"fin ({fin_char}){completion_str}" + ) + + self.file_logger.info(f"{first_str} | {second_str} | {third_str}") + + def log_progress_to_console_from_status( + self, + status: VerificationStatus, + req_id: RequestId, + subservice: Optional[pus_1.Subservices] = None, + ): + if self.console_logger is None: + raise ValueError("Invalid console logger") + acc_char = gen_console_char_from_status(status.accepted, self.with_colors) + start_char = gen_console_char_from_status(status.started, self.with_colors) + step_char = gen_console_char_from_status(status.step, self.with_colors) + fin_char = gen_console_char_from_status(status.completed, self.with_colors) + step_num = self.step_num(status) + first_str = self._get_info_string(subservice) + second_str = f"Request ID {req_id.as_u32():#04x}" + completion_str = "" + if status.completed == StatusField.SUCCESS: + completion_str = f" {AnsiColors.BOLD}{AnsiColors.YELLOW}\u2728" + third_str = ( + f"acc ({acc_char}) sta ({start_char}) ste ({step_char}, {step_num}) " + f"fin ({fin_char}){completion_str}" + ) + self.console_logger.info(f"{first_str} | {second_str} | {third_str}") + + @staticmethod + def step_num(status: VerificationStatus): + if not status.step_list: + return "0" + else: + return f"{max(status.step_list)}" + + @staticmethod + def _get_info_string(subservice: pus_1.Subservices): + status_str = "Status" + if subservice is not None: + if subservice == pus_1.Subservices.TM_ACCEPTANCE_SUCCESS: + status_str = "Acceptance success" + elif subservice == pus_1.Subservices.TM_ACCEPTANCE_FAILURE: + status_str = "Acceptance failure" + elif subservice == pus_1.Subservices.TM_START_SUCCESS: + status_str = "Start success" + elif subservice == pus_1.Subservices.TM_START_FAILURE: + status_str = "Start failure" + elif subservice == pus_1.Subservices.TM_STEP_SUCCESS: + status_str = "Step success" + elif subservice == pus_1.Subservices.TM_STEP_FAILURE: + status_str = "Step failure" + elif subservice == pus_1.Subservices.TM_COMPLETION_SUCCESS: + status_str = "Completion success" + elif subservice == pus_1.Subservices.TM_COMPLETION_FAILURE: + status_str = "Completion failure" + return f"{status_str} of TC".ljust(25) + + +def gen_file_char_from_status(status: StatusField): + if status == StatusField.UNSET: + return "-" + elif status == StatusField.FAILURE: + return "F" + elif status == StatusField.SUCCESS: + return "S" + + +def gen_console_char_from_status(status: StatusField, with_color: bool): + if status == StatusField.UNSET: + return dash_unicode(with_color) + elif status == StatusField.SUCCESS: + return tick_mark_unicode(with_color) + elif status == StatusField.FAILURE: + return cross_mark_unicode(with_color) + + +def dash_unicode(with_color: bool) -> str: + if with_color: + return f"{AnsiColors.YELLOW}-{AnsiColors.RESET}" + else: + return "-" + + +def tick_mark_unicode(with_color: bool) -> str: + if with_color: + return f"{AnsiColors.GREEN}\u2713{AnsiColors.RESET}" + else: + return "\u2713" + + +def cross_mark_unicode(with_color: bool) -> str: + if with_color: + return f"{AnsiColors.RED}\u274c{AnsiColors.RESET}" + else: + return "\u274c" diff --git a/src/tmtccmd/pus/pus_11_tc_sched.py b/tmtccmd/pus/pus_11_tc_sched.py similarity index 95% rename from src/tmtccmd/pus/pus_11_tc_sched.py rename to tmtccmd/pus/pus_11_tc_sched.py index a79736a9..f2c7395f 100644 --- a/src/tmtccmd/pus/pus_11_tc_sched.py +++ b/tmtccmd/pus/pus_11_tc_sched.py @@ -66,4 +66,4 @@ def __repr__(self): @classmethod def build_from_tc(cls, tc: PusTelecommand) -> TcSchedReqId: - return TcSchedReqId(tc.data_field_header.source_id, tc.apid, tc.ssc) + return TcSchedReqId(tc.pus_tc_sec_header.source_id, tc.apid, tc.seq_count) diff --git a/tmtccmd/pus/pus_17_test.py b/tmtccmd/pus/pus_17_test.py new file mode 100644 index 00000000..aaf665ca --- /dev/null +++ b/tmtccmd/pus/pus_17_test.py @@ -0,0 +1,53 @@ +from __future__ import annotations +import enum + +from spacepackets.ecss import PusTelecommand +from spacepackets.ecss.conf import get_default_tc_apid +from spacepackets.ecss.pus_17_test import Subservices +from tmtccmd.tc.queue import QueueHelper + + +class CustomSubservices(enum.IntEnum): + TC_GEN_EVENT = 128 + + +def pack_service_17_ping_command(ssc: int, apid: int = -1) -> PusTelecommand: + """Generate a simple ping PUS telecommand packet""" + if apid == -1: + apid = get_default_tc_apid() + return PusTelecommand( + service=17, subservice=Subservices.TC_PING.value, seq_count=ssc, apid=apid + ) + + +def pack_generic_service17_test(init_ssc: int, q: QueueHelper, apid: int = -1) -> int: + if apid == -1: + apid = get_default_tc_apid() + new_ssc = init_ssc + q.add_log_cmd("Testing Service 17") + # ping test + q.add_log_cmd("Testing Service 17: Ping Test") + q.add_pus_tc(pack_service_17_ping_command(ssc=new_ssc)) + new_ssc += 1 + # enable event + q.add_log_cmd("Testing Service 17: Enable Event") + q.add_pus_tc(PusTelecommand(service=5, subservice=5, seq_count=new_ssc, apid=apid)) + new_ssc += 1 + # test event + q.add_log_cmd("Testing Service 17: Trigger event") + q.add_pus_tc( + PusTelecommand( + service=17, + subservice=CustomSubservices.TC_GEN_EVENT, + seq_count=new_ssc, + apid=apid, + ) + ) + new_ssc += 1 + # invalid subservice + q.add_log_cmd("Testing Service 17: Invalid subservice") + q.add_pus_tc( + PusTelecommand(service=17, subservice=243, seq_count=new_ssc, apid=apid) + ) + new_ssc += 1 + return new_ssc diff --git a/src/tmtccmd/pus/pus_200_fsfw_mode.py b/tmtccmd/pus/pus_200_fsfw_mode.py similarity index 100% rename from src/tmtccmd/pus/pus_200_fsfw_mode.py rename to tmtccmd/pus/pus_200_fsfw_mode.py diff --git a/src/tmtccmd/pus/pus_201_fsfw_health.py b/tmtccmd/pus/pus_201_fsfw_health.py similarity index 100% rename from src/tmtccmd/pus/pus_201_fsfw_health.py rename to tmtccmd/pus/pus_201_fsfw_health.py diff --git a/tmtccmd/pus/pus_20_params.py b/tmtccmd/pus/pus_20_params.py new file mode 100644 index 00000000..399be1c9 --- /dev/null +++ b/tmtccmd/pus/pus_20_params.py @@ -0,0 +1,6 @@ +import enum + + +class CustomSubservices(enum.IntEnum): + LOAD = 128 + DUMP = 129 diff --git a/src/tmtccmd/pus/pus_5_event.py b/tmtccmd/pus/pus_5_event.py similarity index 100% rename from src/tmtccmd/pus/pus_5_event.py rename to tmtccmd/pus/pus_5_event.py diff --git a/src/tmtccmd/pus/pus_8_funccmd.py b/tmtccmd/pus/pus_8_funccmd.py similarity index 100% rename from src/tmtccmd/pus/pus_8_funccmd.py rename to tmtccmd/pus/pus_8_funccmd.py diff --git a/tmtccmd/pus/seqcnt.py b/tmtccmd/pus/seqcnt.py new file mode 100644 index 00000000..8f8f7c6c --- /dev/null +++ b/tmtccmd/pus/seqcnt.py @@ -0,0 +1,59 @@ +from abc import abstractmethod, ABC +from pathlib import Path + + +class ProvidesSeqCount(ABC): + @abstractmethod + def next_seq_count(self) -> int: + raise NotImplementedError( + "Please use a concrete class implementing this method" + ) + + +class FileSeqCountProvider(ProvidesSeqCount): + def __init__(self, file_name: Path = Path("seqcnt.txt")): + self.file_name = file_name + if not self.file_name.exists(): + self.create_new() + + def create_new(self): + with open(self.file_name, "w") as file: + file.write("0\n") + + def current(self) -> int: + if not self.file_name.exists(): + raise FileNotFoundError(f"{self.file_name} file does not exist") + with open(self.file_name) as file: + return self.check_count(file.readline()) + + def __next__(self): + if not self.file_name.exists(): + raise FileNotFoundError(f"{self.file_name} file does not exist") + with open(self.file_name, "r+") as file: + curr_seq_cnt = self.increment_with_rollover( + self.check_count(file.readline()) + ) + file.seek(0) + file.write(f"{curr_seq_cnt}\n") + return curr_seq_cnt + + @staticmethod + def check_count(line: str) -> int: + line = line.rstrip() + if not line.isdigit(): + raise ValueError("Sequence count file content is invalid") + curr_seq_cnt = int(line) + if curr_seq_cnt < 0 or curr_seq_cnt > pow(2, 14) - 1: + raise ValueError("Sequence count in file has invalid value") + return curr_seq_cnt + + def next_seq_count(self) -> int: + return self.__next__() + + @staticmethod + def increment_with_rollover(seq_cnt: int) -> int: + """CCSDS Sequence count has maximum size of 14 bit. Rollover after that size by default""" + if seq_cnt >= pow(2, 14) - 1: + return 0 + else: + return seq_cnt + 1 diff --git a/tmtccmd/tc/__init__.py b/tmtccmd/tc/__init__.py new file mode 100644 index 00000000..20747b35 --- /dev/null +++ b/tmtccmd/tc/__init__.py @@ -0,0 +1,22 @@ +from .queue import ( + QueueHelper, + QueueWrapper, + TcQueueEntryType, + TcQueueEntryBase, + QueueEntryHelper, + WaitEntry, + SpacePacketEntry, + PusTcEntry, + RawTcEntry, + PacketDelayEntry, + LogQueueEntry, +) +from .procedure import ( + TcProcedureBase, + TcProcedureType, + DefaultProcedureInfo, + CustomProcedureInfo, + ProcedureHelper, +) + +from .handler import FeedWrapper, TcHandlerBase diff --git a/tmtccmd/tc/ccsds_seq_sender.py b/tmtccmd/tc/ccsds_seq_sender.py new file mode 100644 index 00000000..93dab494 --- /dev/null +++ b/tmtccmd/tc/ccsds_seq_sender.py @@ -0,0 +1,189 @@ +"""Used to send multiple TCs in sequence""" +import enum +from datetime import timedelta +from typing import Optional + +from tmtccmd.tc import ( + TcQueueEntryBase, + TcQueueEntryType, + QueueEntryHelper, + ProcedureHelper, +) +from tmtccmd.tc.handler import TcHandlerBase +from tmtccmd.tc.queue import QueueWrapper +from tmtccmd.com_if import ComInterface +from tmtccmd.logging import get_console_logger +from tmtccmd.utility.countdown import Countdown + +LOGGER = get_console_logger() + + +class SenderMode(enum.IntEnum): + BUSY = 0 + DONE = 1 + + +class SeqResultWrapper: + def __init__(self, mode: SenderMode): + self.mode = mode + self.longest_rem_delay: timedelta = timedelta() + self.tc_sent: bool = False + + +class SequentialCcsdsSender: + """Specific implementation of CommandSenderReceiver to send multiple telecommands in sequence""" + + def __init__( + self, + queue_wrapper: QueueWrapper, + tc_handler: TcHandlerBase, + ): + """ + :param queue_wrapper: Wrapper object containing the queue and queue handling properties + :param tc_handler: + """ + self._tc_handler = tc_handler + self._queue_wrapper = queue_wrapper + self._mode = SenderMode.DONE + self._wait_cd = Countdown(None) + self._send_cd = Countdown(queue_wrapper.inter_cmd_delay) + self._current_res = SeqResultWrapper(self._mode) + self._current_res.longest_rem_delay = queue_wrapper.inter_cmd_delay + self._op_divider = 0 + self._last_queue_entry: Optional[TcQueueEntryBase] = None + self._last_tc: Optional[TcQueueEntryBase] = None + + @property + def queue_wrapper(self): + return self._queue_wrapper + + @queue_wrapper.setter + def queue_wrapper(self, queue_wrapper: QueueWrapper): + """This setter throws a ValueError if the sequential sender is busy with another queue""" + if self._mode == SenderMode.BUSY: + raise ValueError("Busy with other queue") + self._mode = SenderMode.BUSY + # There is no need to delay sending of the first entry, the send delay is inter-packet + # only + self._send_cd.timeout = timedelta() + self._queue_wrapper = queue_wrapper + + def handle_new_queue_forced(self, queue_wrapper: QueueWrapper): + self._mode = SenderMode.DONE + self.queue_wrapper = queue_wrapper + + def resume(self): + """Can be used to resume a finished sequential sender it the provided queue is + not empty anymore""" + if self._mode == SenderMode.DONE and self.queue_wrapper.queue: + self._mode = SenderMode.BUSY + + def operation(self, com_if: ComInterface) -> SeqResultWrapper: + """Primary function which should be called periodically to consume a TC queue. + + :param com_if: Communication interface used to send telecommands. Will be passed to the + user send function + """ + self._handle_current_tc_queue(com_if) + self._current_res.mode = self._mode + return self._current_res + + @property + def mode(self): + return self._mode + + def _handle_current_tc_queue(self, com_if: ComInterface): + """Primary function which is called for sequential transfer. + :return: + """ + # Do not use continue anywhere in this while loop for now + if not self.queue_wrapper.queue: + if self.no_delay_remaining(): + # cache this for last wait time + self._tc_handler.queue_finished_cb( + ProcedureHelper(self._queue_wrapper.info) + ) + self._mode = SenderMode.DONE + return + else: + self._check_next_telecommand(com_if) + self.__print_rem_timeout(op_divider=self._op_divider) + self._op_divider += 1 + + def __print_rem_timeout(self, op_divider: int, divisor: int = 15): + if not self.__wait_cd_timed_out() and op_divider % divisor == 0: + rem_time = self._wait_cd.rem_time() + if self._wait_cd.rem_time() > timedelta(): + LOGGER.info( + f"{rem_time.total_seconds():.01f} seconds wait time remaining" + ) + + def _check_next_telecommand(self, com_if: ComInterface): + """Sends the next telecommand and returns whether an actual telecommand was sent""" + next_queue_entry = self.queue_wrapper.queue[0] + is_tc = self.handle_non_tc_entry(next_queue_entry) + call_send_cb = True + if is_tc: + if self.no_delay_remaining(): + self._current_res.tc_sent = True + else: + self._current_res.tc_sent = False + call_send_cb = False + else: + self._current_res.tc_sent = False + if call_send_cb: + self._tc_handler.send_cb(QueueEntryHelper(next_queue_entry), com_if) + if is_tc: + if self.queue_wrapper.inter_cmd_delay != self._send_cd.timeout: + self._send_cd.reset(self.queue_wrapper.inter_cmd_delay) + else: + self._send_cd.reset() + self.queue_wrapper.queue.popleft() + if not self.queue_wrapper.queue and self.no_delay_remaining(): + self._tc_handler.queue_finished_cb( + ProcedureHelper(self._queue_wrapper.info) + ) + self._mode = SenderMode.DONE + + def no_delay_remaining(self) -> bool: + return self.__send_cd_timed_out() and self.__wait_cd_timed_out() + + def __send_cd_timed_out(self): + """Internal wrapper API to allow easier testing""" + return self._send_cd.timed_out() + + def __wait_cd_timed_out(self): + """Internal wrapper API to allow easier testing""" + return self._wait_cd.timed_out() + + def handle_non_tc_entry(self, queue_entry: TcQueueEntryBase) -> bool: + """ + Checks whether the entry in the pus_tc queue is a telecommand. + :param queue_entry: Generic queue entry + :return: True if queue entry is telecommand, False if it is not + """ + if not isinstance(queue_entry, TcQueueEntryBase): + LOGGER.warning("Invalid queue entry detected") + raise ValueError("Invalid queue entry detected") + cast_wrapper = QueueEntryHelper(queue_entry) + if queue_entry.etype == TcQueueEntryType.WAIT: + wait_entry = cast_wrapper.to_wait_entry() + LOGGER.info( + f"Waiting for {wait_entry.wait_time.total_seconds() * 1000} milliseconds." + ) + self._wait_cd.reset(new_timeout=wait_entry.wait_time) + self._current_res.longest_rem_delay = max( + self._wait_cd.rem_time(), self._send_cd.rem_time() + ) + elif queue_entry.etype == TcQueueEntryType.PACKET_DELAY: + timeout_entry = cast_wrapper.to_packet_delay_entry() + self.queue_wrapper.inter_cmd_delay = timeout_entry.delay_time + self._send_cd.reset(new_timeout=timeout_entry.delay_time) + self._current_res.longest_rem_delay = max( + self._wait_cd.rem_time(), self._send_cd.rem_time() + ) + is_tc = queue_entry.is_tc() + if is_tc: + self._last_tc = queue_entry + self._last_queue_entry = queue_entry + return is_tc diff --git a/tmtccmd/tc/handler.py b/tmtccmd/tc/handler.py new file mode 100644 index 00000000..63bb154f --- /dev/null +++ b/tmtccmd/tc/handler.py @@ -0,0 +1,74 @@ +from abc import abstractmethod, ABC + +from tmtccmd.com_if import ComInterface +from tmtccmd.tc import ProcedureHelper +from tmtccmd.tc.queue import QueueHelper, QueueWrapper, QueueEntryHelper + + +class FeedWrapper: + """This class wraps the queue and some additional information and useful fields which + can be set by the user. + + :var queue_helper: Can be used to simplify insertion of queue entries like telecommands into + the queue + :var dispatch_next_queue: Can be used to prevent the dispatch of the queue + :var modes: Currently contains the current TC Mode and TM mode of the calling handler class + """ + + def __init__(self, queue_wrapper: QueueWrapper, auto_dispatch: bool): + from tmtccmd.core import ModeWrapper + + self.queue_helper = QueueHelper(queue_wrapper) + self.dispatch_next_queue = auto_dispatch + self.modes = ModeWrapper() + + +class TcHandlerBase(ABC): + """Generic abstract class for a TC handler object. Should be implemented by the user. + This object then takes care of sending packets by providing the :py:meth:`send_cb` + send-callback. It also provides telecommand queues by providing the :py:meth:`feed_cb` queue + feeder callback. + """ + + def __init__(self): + pass + + @abstractmethod + def send_cb(self, entry_helper: QueueEntryHelper, com_if: ComInterface): + """This function callback will be called for each queue entry. This also includes + miscellaneous queue entries, for example the ones used to log additional information. + It is up to the user code implementation to determine the concrete queue entry and what + to do with it. + + In general, an implementation will perform the following steps: + + 1. Determine the queue entry and what to do with it + 2. If applicable, retrieve the raw data to send from the queue entry and send it using + the generic communication interface + + :param entry_helper: Queue entry base type. The user can cast this back to the concrete + type or just use duck typing if the concrete type is known + :param com_if: Communication interface. Will generally be used to send the packet, + using the :py:func:`tmtccmd.com_if.ComInterface.send` method + """ + pass + + @abstractmethod + def queue_finished_cb(self, info: ProcedureHelper): + pass + + @abstractmethod + def feed_cb(self, info: ProcedureHelper, wrapper: FeedWrapper): + """This function will be called to retrieve a telecommand queue from the user code, based + on a procedure. The passed feed wrapper can be used to set the TC queue or other + parameter like the inter-packet delay. + + :param info: Generic base class for a procedure. For example, the + py:class:`tmtccmd.tc.DefaultProcedureInfo` class uses a service string + and op code string which can be used in the user code to select between different + telecommand queues being packed + :param wrapper: Wrapper type around the queue. It also contains a queue helper class + to simplify adding entries to the telecommand queue + :return: + """ + pass diff --git a/tmtccmd/tc/procedure.py b/tmtccmd/tc/procedure.py new file mode 100644 index 00000000..0bfbfdf8 --- /dev/null +++ b/tmtccmd/tc/procedure.py @@ -0,0 +1,68 @@ +import enum +from typing import Any, cast, Type + + +class TcProcedureType(enum.Enum): + DEFAULT = 0 + CFDP = 1 + CUSTOM = 2 + + +class TcProcedureBase: + def __init__(self, ptype: TcProcedureType): + self.ptype = ptype + + +class CustomProcedureInfo(TcProcedureBase): + def __init__(self, info: any): + super().__init__(TcProcedureType.CUSTOM) + self.info = info + + def __repr__(self): + return f"{self.__class__.__name__}(info={self.info!r}" + + +class DefaultProcedureInfo(TcProcedureBase): + """Generic abstraction for procedures. A procedure can be a single command or a sequence + of commands. Generally, one procedure is mapped to a specific TC queue which is packed + during run-time""" + + def __init__(self, service: str, op_code: str): + super().__init__(TcProcedureType.DEFAULT) + self.service = service + self.op_code = op_code + + def __repr__(self): + return f"CmdInfo(service={self.service!r}, op_code={self.op_code!r})" + + +class ProcedureHelper: + """Procedure helper class. It wraps the concrete procedure object but allows easily casting + it to concrete types supported by the framework.""" + + def __init__(self, base: TcProcedureBase): + self.base = base + + @property + def proc_type(self): + return self.base.ptype + + def __cast_internally( + self, + obj_type: Type[TcProcedureBase], + obj: TcProcedureBase, + expected_type: TcProcedureType, + ) -> Any: + if obj.ptype != expected_type: + raise TypeError(f"Invalid object {obj} for type {self.base.ptype}") + return cast(obj_type, obj) + + def to_def_procedure(self) -> DefaultProcedureInfo: + return self.__cast_internally( + DefaultProcedureInfo, self.base, TcProcedureType.DEFAULT + ) + + def to_custom_procedure(self) -> CustomProcedureInfo: + return self.__cast_internally( + CustomProcedureInfo, self.base, TcProcedureType.CUSTOM + ) diff --git a/src/tmtccmd/tc/pus_11_tc_sched.py b/tmtccmd/tc/pus_11_tc_sched.py similarity index 91% rename from src/tmtccmd/tc/pus_11_tc_sched.py rename to tmtccmd/tc/pus_11_tc_sched.py index efe6d679..4cac6659 100644 --- a/src/tmtccmd/tc/pus_11_tc_sched.py +++ b/tmtccmd/tc/pus_11_tc_sched.py @@ -1,6 +1,6 @@ from spacepackets.ecss import PusTelecommand from spacepackets.ecss.conf import FETCH_GLOBAL_APID -from tmtccmd.pus.definitions import PusServices +from tmtccmd.pus import PusServices from tmtccmd.pus.pus_11_tc_sched import Subservices @@ -8,7 +8,10 @@ def __generic_param_less_tc_sched_cmd( subservice: int, ssc: int, apid: int = -1 ) -> PusTelecommand: return PusTelecommand( - service=PusServices.S11_TC_SCHED, subservice=subservice, ssc=ssc, apid=apid + service=PusServices.S11_TC_SCHED, + subservice=subservice, + seq_count=ssc, + apid=apid, ) @@ -46,7 +49,7 @@ def generate_time_tagged_cmd( service=PusServices.S11_TC_SCHED, subservice=Subservices.TC_INSERT, app_data=pack_time_tagged_tc_app_data(release_time, tc_to_insert), - ssc=ssc, + seq_count=ssc, apid=apid, ) diff --git a/src/tmtccmd/tc/pus_200_fsfw_modes.py b/tmtccmd/tc/pus_200_fsfw_modes.py similarity index 100% rename from src/tmtccmd/tc/pus_200_fsfw_modes.py rename to tmtccmd/tc/pus_200_fsfw_modes.py diff --git a/src/tmtccmd/tc/pus_201_fsfw_health.py b/tmtccmd/tc/pus_201_fsfw_health.py similarity index 100% rename from src/tmtccmd/tc/pus_201_fsfw_health.py rename to tmtccmd/tc/pus_201_fsfw_health.py diff --git a/src/tmtccmd/tc/pus_20_params.py b/tmtccmd/tc/pus_20_params.py similarity index 93% rename from src/tmtccmd/tc/pus_20_params.py rename to tmtccmd/tc/pus_20_params.py index 33c39b3f..1df828ae 100644 --- a/src/tmtccmd/tc/pus_20_params.py +++ b/tmtccmd/tc/pus_20_params.py @@ -2,10 +2,9 @@ """ import struct from typing import Optional + +from spacepackets.ecss.fields import Ptc, PfcUnsigned, PfcReal from tmtccmd.pus.pus_20_params import ( - EcssPtc, - EcssPfcUnsigned, - EcssPfcReal, CustomSubservices, ) from spacepackets.ecss.tc import PusTelecommand @@ -22,7 +21,7 @@ def pack_fsfw_load_param_cmd( subservice=CustomSubservices.LOAD, app_data=app_data, apid=apid, - ssc=ssc, + seq_count=ssc, ) @@ -41,8 +40,8 @@ def pack_boolean_parameter_app_data( object_id=object_id, domain_id=domain_id, unique_id=unique_id, - ptc=EcssPtc.UNSIGNED, - pfc=EcssPfcUnsigned.ONE_BYTE, + ptc=Ptc.UNSIGNED, + pfc=PfcUnsigned.ONE_BYTE, rows=1, columns=1, ) @@ -58,8 +57,8 @@ def pack_scalar_double_param_app_data( object_id=object_id, domain_id=domain_id, unique_id=unique_id, - ptc=EcssPtc.REAL, - pfc=EcssPfcReal.DOUBLE_PRECISION_IEEE, + ptc=Ptc.REAL, + pfc=PfcReal.DOUBLE_PRECISION_IEEE, rows=1, columns=1, ) @@ -75,8 +74,8 @@ def pack_scalar_float_param_app_data( object_id=object_id, domain_id=domain_id, unique_id=unique_id, - ptc=EcssPtc.REAL, - pfc=EcssPfcReal.FLOAT_SIMPLE_PRECISION_IEEE, + ptc=Ptc.REAL, + pfc=PfcReal.FLOAT_SIMPLE_PRECISION_IEEE, rows=1, columns=1, ) @@ -89,7 +88,7 @@ def prepare_param_packet_header( object_id: bytes, domain_id: int, unique_id: int, - ptc: EcssPtc, + ptc: Ptc, pfc: int, rows: int, columns: int, @@ -117,6 +116,7 @@ def pack_type_and_matrix_data(ptc: int, pfc: int, rows: int, columns: int) -> by number of columns and rows in the parameter. See https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/ p.428 for more information. + :param ptc: ECSS PTC number :param pfc: ECSS PFC number :param rows: Number of rows in parameter (for matrix entries, 1 for vector entries, diff --git a/src/tmtccmd/tc/pus_3_fsfw_hk.py b/tmtccmd/tc/pus_3_fsfw_hk.py similarity index 90% rename from src/tmtccmd/tc/pus_3_fsfw_hk.py rename to tmtccmd/tc/pus_3_fsfw_hk.py index fce12efa..e28013a9 100644 --- a/src/tmtccmd/tc/pus_3_fsfw_hk.py +++ b/tmtccmd/tc/pus_3_fsfw_hk.py @@ -46,7 +46,9 @@ def __generate_periodic_hk_command( subservice = Subservices.TC_DISABLE_PERIODIC_DIAGNOSTICS_GEN else: subservice = Subservices.TC_DISABLE_PERIODIC_HK_GEN - return PusTelecommand(service=3, subservice=subservice, ssc=ssc, app_data=app_data) + return PusTelecommand( + service=3, subservice=subservice, seq_count=ssc, app_data=app_data + ) def modify_collection_interval( @@ -58,14 +60,16 @@ def modify_collection_interval( subservice = Subservices.TC_MODIFY_DIAGNOSTICS_REPORT_COLLECTION_INTERVAL else: subservice = Subservices.TC_MODIFY_PARAMETER_REPORT_COLLECTION_INTERVAL - return PusTelecommand(service=3, subservice=subservice, ssc=ssc, app_data=app_data) + return PusTelecommand( + service=3, subservice=subservice, seq_count=ssc, app_data=app_data + ) def generate_one_hk_command(sid: bytes, ssc: int) -> PusTelecommand: return PusTelecommand( service=3, subservice=Subservices.TC_GENERATE_ONE_PARAMETER_REPORT, - ssc=ssc, + seq_count=ssc, app_data=sid, ) @@ -74,6 +78,6 @@ def generate_one_diag_command(sid: bytes, ssc: int) -> PusTelecommand: return PusTelecommand( service=3, subservice=Subservices.TC_GENERATE_ONE_DIAGNOSTICS_REPORT, - ssc=ssc, + seq_count=ssc, app_data=sid, ) diff --git a/tmtccmd/tc/pus_5_event.py b/tmtccmd/tc/pus_5_event.py new file mode 100644 index 00000000..7ffc46c8 --- /dev/null +++ b/tmtccmd/tc/pus_5_event.py @@ -0,0 +1,50 @@ +"""Contains definitions and functions related to PUS Service 5 Telecommands. +""" +from spacepackets.ecss import PusTelecommand +from spacepackets.ecss.conf import get_default_tc_apid +from spacepackets.ecss.pus_5_event import Subservices + +from tmtccmd.tc.queue import QueueHelper + + +def pack_enable_event_reporting_command(ssc: int, apid: int = -1): + if apid == -1: + apid = get_default_tc_apid() + return PusTelecommand( + service=5, + subservice=Subservices.TC_ENABLE_EVENT_REPORTING, + seq_count=ssc, + apid=apid, + ) + + +def pack_disable_event_reporting_command(ssc: int, apid: int = -1): + if apid == -1: + apid = get_default_tc_apid() + return PusTelecommand( + service=5, + subservice=Subservices.TC_DISABLE_EVENT_REPORTING, + seq_count=ssc, + apid=apid, + ) + + +def pack_generic_service5_test_into(q: QueueHelper, apid: int = -1): + if apid == -1: + apid = get_default_tc_apid() + q.add_log_cmd("Testing Service 5") + # invalid subservice + q.add_log_cmd("Testing Service 5: Invalid subservice") + q.add_pus_tc(PusTelecommand(service=5, subservice=1, apid=apid, seq_count=500)) + # disable events + q.add_log_cmd("Testing Service 5: Disable event") + q.add_pus_tc(pack_disable_event_reporting_command(ssc=501)) + # trigger event + q.add_log_cmd("Testing Service 5: Trigger event") + q.add_pus_tc(PusTelecommand(service=17, subservice=128, apid=apid, seq_count=510)) + # enable event + q.add_log_cmd("Testing Service 5: Enable event") + q.add_pus_tc(pack_enable_event_reporting_command(ssc=520)) + # trigger event + q.add_log_cmd("Testing Service 5: Trigger another event") + q.add_pus_tc(PusTelecommand(service=17, subservice=128, apid=apid, seq_count=530)) diff --git a/src/tmtccmd/tc/pus_8_funccmd.py b/tmtccmd/tc/pus_8_funccmd.py similarity index 89% rename from src/tmtccmd/tc/pus_8_funccmd.py rename to tmtccmd/tc/pus_8_funccmd.py index 01964b79..85b88dfe 100644 --- a/src/tmtccmd/tc/pus_8_funccmd.py +++ b/tmtccmd/tc/pus_8_funccmd.py @@ -1,7 +1,6 @@ -import enum import struct -from tmtccmd.tc.definitions import PusTelecommand +from spacepackets.ecss import PusTelecommand from tmtccmd.pus.pus_8_funccmd import Subservices from spacepackets.ecss.conf import get_default_tc_apid @@ -20,7 +19,7 @@ def generate_action_command( return PusTelecommand( service=8, subservice=Subservices.FUNCTIONAL_CMD, - ssc=ssc, + seq_count=ssc, app_data=data_to_pack, apid=apid, ) diff --git a/tmtccmd/tc/queue.py b/tmtccmd/tc/queue.py new file mode 100644 index 00000000..8493571c --- /dev/null +++ b/tmtccmd/tc/queue.py @@ -0,0 +1,195 @@ +from __future__ import annotations +from datetime import timedelta +from enum import Enum +from typing import Optional, Deque, cast, Any, Type + +from spacepackets.ccsds import SpacePacket +from spacepackets.ecss import PusTelecommand +from tmtccmd.tc.procedure import TcProcedureBase + + +class TcQueueEntryType(Enum): + PUS_TC = "pus-tc" + CCSDS_TC = "ccsds-tc" + RAW_TC = "raw-tc" + CUSTOM = "custom" + LOG = "log" + WAIT = "wait" + PACKET_DELAY = "set-delay" + + +class TcQueueEntryBase: + """Generic TC queue entry abstraction. This allows filling the TC queue with custom objects""" + + def __init__(self, etype: TcQueueEntryType): + self.etype = etype + + def is_tc(self) -> bool: + """Check whether concrete object is an actual telecommand""" + if ( + self.etype == TcQueueEntryType.PUS_TC + or self.etype == TcQueueEntryType.RAW_TC + or self.etype == TcQueueEntryType.CCSDS_TC + ): + return True + return False + + +QueueDequeT = Deque[TcQueueEntryBase] + + +class PusTcEntry(TcQueueEntryBase): + def __init__(self, pus_tc: PusTelecommand): + super().__init__(TcQueueEntryType.PUS_TC) + self.pus_tc = pus_tc + + def __repr__(self): + return f"{self.__class__.__name__}({self.pus_tc!r})" + + +class SpacePacketEntry(TcQueueEntryBase): + def __init__(self, space_packet: SpacePacket): + super().__init__(TcQueueEntryType.CCSDS_TC) + self.space_packet = space_packet + + def __repr__(self): + return f"{self.__class__.__name__}({self.space_packet!r})" + + +class LogQueueEntry(TcQueueEntryBase): + def __init__(self, log_str: str): + super().__init__(TcQueueEntryType.LOG) + self.log_str = log_str + + def __repr__(self): + return f"{self.__class__.__name__}({self.log_str!r})" + + +class RawTcEntry(TcQueueEntryBase): + def __init__(self, tc: bytes): + super().__init__(TcQueueEntryType.RAW_TC) + self.tc = tc + + def __repr__(self): + return f"{self.__class__.__name__}({self.tc!r})" + + +class WaitEntry(TcQueueEntryBase): + def __init__(self, wait_time: timedelta): + super().__init__(TcQueueEntryType.WAIT) + self.wait_time = wait_time + + @classmethod + def from_millis(cls, millis: int) -> WaitEntry: + return cls(timedelta(milliseconds=millis)) + + def __repr__(self): + return f"{self.__class__.__name__}({self.wait_time!r})" + + +class PacketDelayEntry(TcQueueEntryBase): + def __init__(self, delay_time: timedelta): + super().__init__(TcQueueEntryType.PACKET_DELAY) + self.delay_time = delay_time + + @classmethod + def from_millis(cls, millis: int) -> PacketDelayEntry: + return cls(timedelta(milliseconds=millis)) + + def __repr__(self): + return f"{self.__class__.__name__}({self.delay_time!r})" + + +class QueueEntryHelper: + def __init__(self, base: TcQueueEntryBase): + self.base = base + + @property + def is_tc(self) -> bool: + return self.base.is_tc() + + @property + def entry_type(self) -> TcQueueEntryType: + return self.base.etype + + def __cast_internally( + self, + obj_type: Type[TcQueueEntryBase], + obj: TcQueueEntryBase, + expected_type: TcQueueEntryType, + ) -> Any: + if obj.etype != expected_type: + raise TypeError(f"Invalid object {obj} for type {self.base.etype}") + return cast(obj_type, obj) + + def to_log_entry(self) -> LogQueueEntry: + return self.__cast_internally(LogQueueEntry, self.base, TcQueueEntryType.LOG) + + def to_pus_tc_entry(self) -> PusTcEntry: + return self.__cast_internally(PusTcEntry, self.base, TcQueueEntryType.PUS_TC) + + def to_raw_tc_entry(self) -> RawTcEntry: + return self.__cast_internally(RawTcEntry, self.base, TcQueueEntryType.RAW_TC) + + def to_wait_entry(self) -> WaitEntry: + return self.__cast_internally(WaitEntry, self.base, TcQueueEntryType.WAIT) + + def to_packet_delay_entry(self) -> PacketDelayEntry: + return self.__cast_internally( + PacketDelayEntry, self.base, TcQueueEntryType.PACKET_DELAY + ) + + def to_space_packet_entry(self) -> SpacePacketEntry: + return self.__cast_internally( + SpacePacketEntry, self.base, TcQueueEntryType.CCSDS_TC + ) + + +class QueueWrapper: + def __init__( + self, + info: Optional[TcProcedureBase], + queue: Optional[QueueDequeT], + inter_cmd_delay: timedelta = timedelta(milliseconds=0), + ): + self.info = info + self.queue = queue + self.inter_cmd_delay = inter_cmd_delay + + def __repr__(self): + return ( + f"{self.__class__.__name__}(info={self.info!r}, queue={self.queue!r}, " + f"inter_cmd_delay={self.inter_cmd_delay!r})" + ) + + +class QueueHelper: + def __init__(self, queue_wrapper: QueueWrapper): + self.queue_wrapper = queue_wrapper + + def __repr__(self): + return f"{self.__class__.__name__}(queue_wrapper={self.queue_wrapper!r})" + + def add_log_cmd(self, print_str: str): + self.queue_wrapper.queue.append(LogQueueEntry(print_str)) + + def add_pus_tc(self, pus_tc: PusTelecommand): + self.queue_wrapper.queue.append(PusTcEntry(pus_tc)) + + def add_ccsds_tc(self, space_packet: SpacePacket): + self.queue_wrapper.queue.append(SpacePacketEntry(space_packet)) + + def add_raw_tc(self, tc: bytes): + self.queue_wrapper.queue.append(RawTcEntry(tc)) + + def add_wait(self, wait_time: timedelta): + self.queue_wrapper.queue.append(WaitEntry(wait_time)) + + def add_wait_ms(self, wait_ms: int): + self.queue_wrapper.queue.append(WaitEntry.from_millis(wait_ms)) + + def add_packet_delay(self, delay: timedelta): + self.queue_wrapper.queue.append(PacketDelayEntry(delay)) + + def add_packet_delay_ms(self, delay_ms: int): + self.queue_wrapper.queue.append(PacketDelayEntry.from_millis(delay_ms)) diff --git a/tmtccmd/tm/__init__.py b/tmtccmd/tm/__init__.py new file mode 100644 index 00000000..3d3bf987 --- /dev/null +++ b/tmtccmd/tm/__init__.py @@ -0,0 +1,117 @@ +import enum +from abc import abstractmethod, ABC +from typing import Deque, List, Union, Dict, Optional + +from spacepackets.ecss import PusTelemetry +from tmtccmd.logging import get_console_logger +from tmtccmd.tm.base import PusTmInfoInterface, PusTmInterface +from tmtccmd.tm.pus_5_event import Service5Tm +from tmtccmd.tm.pus_8_funccmd import Service8FsfwTm +from tmtccmd.tm.pus_3_fsfw_hk import Service3FsfwTm +from tmtccmd.tm.pus_20_fsfw_parameters import Service20FsfwTm +from tmtccmd.tm.pus_200_fsfw_modes import Service200FsfwTm + +TelemetryListT = List[bytes] +TelemetryQueueT = Deque[bytes] + +PusTmQueue = Deque[PusTelemetry] +PusTmListT = List[PusTelemetry] + +PusTmQueueT = Deque[PusTmListT] +PusIFListT = List[Union[PusTmInfoInterface, PusTmInterface]] +PusIFQueueT = Deque[PusIFListT] + + +LOGGER = get_console_logger() + + +class TmTypes(enum.Enum): + NONE = enum.auto + CCSDS_SPACE_PACKETS = enum.auto + + +class TmHandlerBase: + def __init__(self, tm_type: TmTypes): + self._tm_type = tm_type + + def get_type(self): + return self._tm_type + + +class SpecificApidHandlerBase(ABC): + """Abstract base class for an CCSDS APID specific handler. The user can implement a TM handler + by implementing this class and then adding it to the :py:class:`CcsdsTmHandler`. + If a CCSDS space packet with a specific APID is received, it will be routed to this handler + using the :py:func:`handle_tm` callback function + """ + + def __init__(self, apid: int, user_args: any): + self.apid = apid + self.user_args: any = user_args + + @abstractmethod + def handle_tm(self, _packet: bytes, _user_args: any): + LOGGER.warning(f"No TM handling implemented for APID {self.apid}") + + +class GenericApidHandlerBase(ABC): + """This class is similar to the :py:class:`SpecificApidHandlerBase` but it is not specific + for an APID and the found APID will be passed to the callback + """ + + def __init__(self, user_args: any): + self.user_args: any = user_args + + @abstractmethod + def handle_tm(self, apid: int, _packet: bytes, _user_args: any): + pass + + +class DefaultApidHandler(GenericApidHandlerBase): + def handle_tm(self, apid: int, _packet: bytes, _user_args: any): + LOGGER.warning(f"No TM handling implemented for unknown APID {apid}") + + +HandlerDictT = Dict[int, SpecificApidHandlerBase] + + +class CcsdsTmHandler(TmHandlerBase): + """Generic CCSDS handler class. The user can create an instance of this class to handle + CCSDS packets by adding dedicated APID handlers or a generic handler for all APIDs with no + dedicated handler""" + + def __init__(self, generic_handler: Optional[GenericApidHandlerBase]): + super().__init__(tm_type=TmTypes.CCSDS_SPACE_PACKETS) + self._handler_dict: HandlerDictT = dict() + if generic_handler is None: + self.generic_handler = DefaultApidHandler(None) + else: + self.generic_handler = generic_handler + + def add_apid_handler(self, handler: SpecificApidHandlerBase): + """Add a TM handler for a certain APID. The handler is a callback function which + will be called if telemetry with that APID arrives. + + :param handler: Handler class instance + :return: + """ + self._handler_dict[handler.apid] = handler + + def has_apid(self, apid: int) -> bool: + return apid in self._handler_dict + + def handle_packet(self, apid: int, packet: bytes) -> bool: + """Handle a packet with an APID. If a handler exists for the given APID, + it is used to handle the packet. If not, a dedicated handler for unknown APIDs + is called. + + :param apid: + :param packet: + :return: True if the packet was passed to as dedicated APID handler, False otherwise + """ + specific_handler = self._handler_dict.get(apid) + if specific_handler is None: + self.generic_handler.handle_tm(apid, packet, self.generic_handler.user_args) + return False + specific_handler.handle_tm(packet, specific_handler.user_args) + return True diff --git a/src/tmtccmd/tm/base.py b/tmtccmd/tm/base.py similarity index 91% rename from src/tmtccmd/tm/base.py rename to tmtccmd/tm/base.py index 5417399b..b0332679 100644 --- a/src/tmtccmd/tm/base.py +++ b/tmtccmd/tm/base.py @@ -83,7 +83,7 @@ def tm_data(self) -> bytearray: @property def ssc(self) -> int: - return self.pus_tm.ssc + return self.pus_tm.seq_count @property def valid(self): @@ -138,15 +138,15 @@ def append_telemetry_content(self, content_list: list): """ content_list.append(f"{self.pus_tm.service}") content_list.append(f"{self.pus_tm.subservice}") - content_list.append(f"{self.pus_tm.secondary_packet_header.message_counter}") + content_list.append(f"{self.pus_tm.pus_tm_sec_header.message_counter}") content_list.append( - f"{self.pus_tm.secondary_packet_header.time.return_unix_seconds()}" + f"{self.pus_tm.pus_tm_sec_header.time.return_unix_seconds()}" ) content_list.append( - f"{self.pus_tm.secondary_packet_header.time.return_time_string()}" + f"{self.pus_tm.pus_tm_sec_header.time.return_time_string()}" ) - content_list.append(f"0x{self.pus_tm.space_packet_header.apid:02x}") - content_list.append(f"{self.pus_tm.space_packet_header.ssc}") + content_list.append(f"0x{self.pus_tm.sp_header.apid:02x}") + content_list.append(f"{self.pus_tm.sp_header.seq_count}") if self.pus_tm.valid: content_list.append("Yes") else: diff --git a/tmtccmd/tm/ccsds_tm_listener.py b/tmtccmd/tm/ccsds_tm_listener.py new file mode 100644 index 00000000..0d5c2742 --- /dev/null +++ b/tmtccmd/tm/ccsds_tm_listener.py @@ -0,0 +1,48 @@ +"""Contains the TmListener which can be used to listen to Telemetry in the background""" +from typing import Dict, List, Tuple + +from spacepackets.ccsds.spacepacket import get_apid_from_raw_space_packet + +from tmtccmd.tm import TelemetryQueueT, CcsdsTmHandler +from tmtccmd.logging import get_console_logger +from tmtccmd.com_if import ComInterface + +LOGGER = get_console_logger() + +INVALID_APID = -2 +UNKNOWN_TARGET_ID = -1 +QueueDictT = Dict[int, Tuple[TelemetryQueueT, int]] +QueueListT = List[Tuple[int, TelemetryQueueT]] + + +class CcsdsTmListener: + """Simple helper object which can be used for retrieving and routing CCSDS packets. + It can be used to poll CCSDS packets from a provided :py:class:`tmtccmd.com_if.ComInterface` + and then route them using a provided CCSDS TM handler. + """ + + def __init__( + self, + tm_handler: CcsdsTmHandler, + ): + """Initiate a TM listener. + + :param tm_handler: If valid CCSDS packets are found, they are dispatched to + the passed handler + """ + self.__tm_handler = tm_handler + + def operation(self, com_if: ComInterface) -> int: + packet_list = com_if.receive() + for tm_packet in packet_list: + self.__handle_ccsds_space_packet(tm_packet) + return len(packet_list) + + def __handle_ccsds_space_packet(self, tm_packet: bytes): + if len(tm_packet) < 6: + LOGGER.warning("TM packet to small to be a CCSDS space packet") + else: + apid = get_apid_from_raw_space_packet(tm_packet) + self.__tm_handler.handle_packet(apid, tm_packet) + return True + return False diff --git a/src/tmtccmd/tm/pus_17_test.py b/tmtccmd/tm/pus_17_test.py similarity index 82% rename from src/tmtccmd/tm/pus_17_test.py rename to tmtccmd/tm/pus_17_test.py index 2cdea53c..0d25c441 100644 --- a/src/tmtccmd/tm/pus_17_test.py +++ b/tmtccmd/tm/pus_17_test.py @@ -1,13 +1,13 @@ from __future__ import annotations from spacepackets.ccsds.time import CdsShortTimestamp from spacepackets.ecss import PusVersion, PusTelemetry -from spacepackets.ecss.pus_17_test import Service17TM +from spacepackets.ecss.pus_17_test import Service17Tm from tmtccmd.tm.base import PusTmInfoBase, PusTmBase from tmtccmd.pus.pus_17_test import Subservices -class Service17TMExtended(PusTmBase, PusTmInfoBase, Service17TM): +class Service17TmExtended(PusTmBase, PusTmInfoBase, Service17Tm): def __init__( self, subservice: int, @@ -21,7 +21,7 @@ def __init__( space_time_ref: int = 0b0000, destination_id: int = 0, ): - Service17TM.__init__( + Service17Tm.__init__( self, subservice=subservice, time=time, @@ -39,7 +39,7 @@ def __init__( self.__set_internal_fields() @classmethod - def __empty(cls) -> Service17TMExtended: + def __empty(cls) -> Service17TmExtended: return cls(subservice=0) def __set_internal_fields(self): @@ -51,10 +51,8 @@ def unpack( cls, raw_telemetry: bytes, pus_version: PusVersion = PusVersion.GLOBAL_CONFIG, - ) -> Service17TMExtended: + ) -> Service17TmExtended: service_17_tm = cls.__empty() - service_17_tm.pus_tm = PusTelemetry.unpack( - raw_telemetry=raw_telemetry, pus_version=pus_version - ) + service_17_tm.pus_tm = PusTelemetry.unpack(raw_telemetry=raw_telemetry) service_17_tm.__set_internal_fields() return service_17_tm diff --git a/src/tmtccmd/tm/pus_1_verification.py b/tmtccmd/tm/pus_1_verification.py similarity index 60% rename from src/tmtccmd/tm/pus_1_verification.py rename to tmtccmd/tm/pus_1_verification.py index 3369c456..0274dfc5 100644 --- a/src/tmtccmd/tm/pus_1_verification.py +++ b/tmtccmd/tm/pus_1_verification.py @@ -1,10 +1,17 @@ from __future__ import annotations + +import struct from abc import abstractmethod -from typing import Deque +from typing import Deque, Optional from spacepackets.ccsds.time import CdsShortTimestamp from spacepackets.ecss.tm import PusVersion, PusTelemetry -from spacepackets.ecss.pus_1_verification import Service1TM, Subservices +from spacepackets.ecss.pus_1_verification import ( + Service1Tm, + Subservices, + VerificationParams, + UnpackParams, +) from tmtccmd.tm.base import PusTmInfoBase, PusTmBase from tmtccmd.logging import get_console_logger @@ -12,98 +19,91 @@ LOGGER = get_console_logger() -class Service1TMExtended(PusTmBase, PusTmInfoBase, Service1TM): - """Service 1 TM class representation. Can be used to deserialize raw service 1 packets.""" +class Service1TmExtended(PusTmBase, PusTmInfoBase, Service1Tm): + """Service 1 TM class representation. Can be used to deserialize raw service 1 packets. + Only PUS C is supported. + TODO: Do not use subclassing here, use a wrapper class.. + """ def __init__( self, - subservice: int, + subservice: Subservices, + verif_params: Optional[VerificationParams] = None, time: CdsShortTimestamp = None, - tc_packet_id: int = 0, - tc_psc: int = 0, - ssc: int = 0, - source_data: bytearray = bytearray([]), + seq_count: int = 0, apid: int = -1, packet_version: int = 0b000, - pus_version: PusVersion = PusVersion.GLOBAL_CONFIG, secondary_header_flag: bool = True, space_time_ref: int = 0b0000, destination_id: int = 0, ): - Service1TM.__init__( + Service1Tm.__init__( self, + verif_params=verif_params, subservice=subservice, time=time, - ssc=ssc, - source_data=source_data, + seq_count=seq_count, apid=apid, packet_version=packet_version, - pus_version=pus_version, secondary_header_flag=secondary_header_flag, space_time_ref=space_time_ref, destination_id=destination_id, ) PusTmBase.__init__(self, pus_tm=self.pus_tm) PusTmInfoBase.__init__(self, pus_tm=self.pus_tm) + self._error_param_1 = 0 + self._error_param_2 = 0 @classmethod - def __empty(cls) -> Service1TMExtended: - return cls(subservice=0) + def __empty(cls) -> Service1TmExtended: + return cls(subservice=Subservices.INVALID) @classmethod - def unpack( - cls, - raw_telemetry: bytes, - pus_version: PusVersion = PusVersion.GLOBAL_CONFIG, - ) -> Service1TMExtended: + def unpack(cls, data: bytes, params: UnpackParams) -> Service1TmExtended: """Parse a service 1 telemetry packet - :param raw_telemetry: - :param pus_version: + :param params: + :param data: :raises ValueError: Raw telemetry too short :return: """ service_1_tm = cls.__empty() - service_1_tm.pus_tm = PusTelemetry.unpack( - raw_telemetry=raw_telemetry, pus_version=pus_version - ) - tm_data = service_1_tm.tm_data - if len(tm_data) < 4: - LOGGER.warning("TM data less than 4 bytes!") - raise ValueError - service_1_tm.tc_packet_id = tm_data[0] << 8 | tm_data[1] - service_1_tm.tc_psc = tm_data[2] << 8 | tm_data[3] - service_1_tm.tc_ssc = service_1_tm.tc_psc & 0x3FFF - if service_1_tm.subservice % 2 == 0: - service_1_tm._handle_failure_verification() - else: - service_1_tm._handle_success_verification() + service_1_tm.pus_tm = PusTelemetry.unpack(raw_telemetry=data) + cls._unpack_raw_tm(service_1_tm, params) + # FSFW specific + if service_1_tm.has_failure_notice: + service_1_tm._error_param_1 = struct.unpack( + "!I", service_1_tm.failure_notice.data[0:4] + ) + service_1_tm._error_param_1 = struct.unpack( + "!I", service_1_tm.failure_notice.data[4:8] + ) return service_1_tm @abstractmethod def append_telemetry_content(self, content_list: list): super().append_telemetry_content(content_list=content_list) - content_list.append(str(hex(self.tc_packet_id))) - content_list.append(str(self.tc_ssc)) - if self.has_tc_error_code: + content_list.append(self.tc_req_id.tc_packet_id) + content_list.append(self.tc_req_id.tc_psc) + if self.has_failure_notice: if self.is_step_reply: - content_list.append(str(self.step_number)) - content_list.append(str(hex(self.error_code))) + content_list.append(str(self.step_id)) + content_list.append(str(hex(self.error_code.val))) content_list.append( - f"hex {self.error_param_1:04x} dec {self.error_param_1}" + f"hex {self.failure_notice:04x} dec {self.failure_notice}" ) content_list.append( - f"hex {self.error_param_2:04x} dec {self.error_param_2}" + f"hex {self.failure_notice.data[0:4]:04x} dec {self._error_param_2}" ) elif self.is_step_reply: - content_list.append(str(self.step_number)) + content_list.append(str(self.step_id)) @abstractmethod def append_telemetry_column_headers(self, header_list: list): super().append_telemetry_column_headers(header_list=header_list) header_list.append("TC Packet ID") - header_list.append("TC SSC") - if self.has_tc_error_code: + header_list.append("TC PSC") + if self.has_failure_notice: if self.is_step_reply: header_list.append("Step Number") header_list.append("Return Value") @@ -112,9 +112,9 @@ def append_telemetry_column_headers(self, header_list: list): elif self.is_step_reply: header_list.append("Step Number") - def _handle_failure_verification(self): + def _unpack_failure_verification(self, params: UnpackParams): """Handle parsing a verification failure packet, subservice ID 2, 4, 6 or 8""" - super()._handle_failure_verification() + super()._unpack_failure_verification(params) self.set_packet_info("Failure Verficiation") subservice = self.pus_tm.subservice if subservice == Subservices.TM_ACCEPTANCE_FAILURE: @@ -126,8 +126,8 @@ def _handle_failure_verification(self): elif subservice == Subservices.TM_COMPLETION_FAILURE: self.append_packet_info(" : Completion Failure") - def _handle_success_verification(self): - super()._handle_success_verification() + def _unpack_success_verification(self, params: UnpackParams): + super()._unpack_success_verification(params) self.set_packet_info("Success Verification") if self.subservice == Subservices.TM_ACCEPTANCE_SUCCESS: self.append_packet_info(" : Acceptance success") @@ -139,4 +139,4 @@ def _handle_success_verification(self): self.append_packet_info(" : Completion success") -PusVerifQueue = Deque[Service1TM] +PusVerifQueue = Deque[Service1Tm] diff --git a/src/tmtccmd/tm/pus_200_fsfw_modes.py b/tmtccmd/tm/pus_200_fsfw_modes.py similarity index 98% rename from src/tmtccmd/tm/pus_200_fsfw_modes.py rename to tmtccmd/tm/pus_200_fsfw_modes.py index 2cdfe1ea..545ff672 100644 --- a/src/tmtccmd/tm/pus_200_fsfw_modes.py +++ b/tmtccmd/tm/pus_200_fsfw_modes.py @@ -4,7 +4,7 @@ import struct from spacepackets.ecss.tm import CdsShortTimestamp, PusVersion, PusTelemetry -from tmtccmd.pus.definitions import CustomPusServices +from tmtccmd.pus import CustomPusServices from tmtccmd.pus.pus_200_fsfw_mode import Subservices from tmtccmd.tm.base import PusTmInfoBase, PusTmBase @@ -39,7 +39,7 @@ def __init__( service=CustomPusServices.SERVICE_200_MODE, subservice=subservice_id, time=time, - ssc=ssc, + seq_count=ssc, source_data=source_data, apid=apid, packet_version=packet_version, diff --git a/src/tmtccmd/tm/pus_20_fsfw_parameters.py b/tmtccmd/tm/pus_20_fsfw_parameters.py similarity index 89% rename from src/tmtccmd/tm/pus_20_fsfw_parameters.py rename to tmtccmd/tm/pus_20_fsfw_parameters.py index fb70e4e4..3200dee6 100644 --- a/src/tmtccmd/tm/pus_20_fsfw_parameters.py +++ b/tmtccmd/tm/pus_20_fsfw_parameters.py @@ -3,15 +3,19 @@ import struct from typing import Optional -from spacepackets.ecss.tm import CdsShortTimestamp, PusVersion, PusTelemetry -from spacepackets.ecss.definitions import PusServices +from spacepackets.ccsds.time import CdsShortTimestamp +from spacepackets.ecss import ( + Ptc, + PfcUnsigned, + PfcSigned, + PfcReal, + PusVersion, + PusTelemetry, +) +from spacepackets.ecss.defs import PusServices from tmtccmd.utility.obj_id import ObjectId from tmtccmd.pus.pus_20_params import ( - EcssPtc, - EcssPfcUnsigned, - EcssPfcReal, - EcssPfcSigned, CustomSubservices, ) from tmtccmd.tm.base import PusTmInfoBase, PusTmBase @@ -57,14 +61,13 @@ def __init__( destination_id: int = 0, ): pus_tm = PusTelemetry( - service=PusServices.SERVICE_20_PARAMETER, + service=PusServices.S20_PARAMETER, subservice=subservice_id, time=time, - ssc=ssc, + seq_count=ssc, source_data=source_data, apid=apid, packet_version=packet_version, - pus_version=pus_version, secondary_header_flag=secondary_header_flag, space_time_ref=space_time_ref, destination_id=destination_id, @@ -135,9 +138,7 @@ def unpack( pus_version: PusVersion = PusVersion.GLOBAL_CONFIG, ) -> Service20FsfwTm: service_20_tm = cls.__empty() - service_20_tm.pus_tm = PusTelemetry.unpack( - raw_telemetry=raw_telemetry, pus_version=pus_version - ) + service_20_tm.pus_tm = PusTelemetry.unpack(raw_telemetry=raw_telemetry) if service_20_tm.custom_fsfw_handling: if len(service_20_tm.pus_tm.tm_data) < 4: LOGGER.warning("Invalid data length, less than 4") @@ -193,23 +194,23 @@ def deserialize_scalar_entry(ptc: int, pfc: int, tm_data: bytes) -> Optional[any len_error_str = "Invalid parameter data size, smaller than " if param_len == 0: return None - if ptc == EcssPtc.UNSIGNED: - if pfc == EcssPfcUnsigned.ONE_BYTE: + if ptc == Ptc.UNSIGNED: + if pfc == PfcUnsigned.ONE_BYTE: if param_len < 1: LOGGER.warning(f"{len_error_str} 1") raise None return tm_data[12] - elif pfc == EcssPfcUnsigned.TWO_BYTES: + elif pfc == PfcUnsigned.TWO_BYTES: if param_len < 2: LOGGER.warning(f"{len_error_str} 2") return None return struct.unpack("!H", tm_data[12:14])[0] - if pfc == EcssPfcUnsigned.FOUR_BYTES: + if pfc == PfcUnsigned.FOUR_BYTES: if param_len < 4: LOGGER.warning(f"{len_error_str} 4") return None return struct.unpack("!I", tm_data[12:16])[0] - elif pfc == EcssPfcUnsigned.EIGHT_BYTES: + elif pfc == PfcUnsigned.EIGHT_BYTES: if param_len < 8: LOGGER.warning(f"{len_error_str} 8") return None @@ -219,23 +220,23 @@ def deserialize_scalar_entry(ptc: int, pfc: int, tm_data: bytes) -> Optional[any f"Parsing of unsigned PTC {ptc} not implemented for PFC {pfc}" ) return None - elif ptc == EcssPtc.SIGNED: - if pfc == EcssPfcSigned.ONE_BYTE: + elif ptc == Ptc.SIGNED: + if pfc == PfcSigned.ONE_BYTE: if param_len < 1: LOGGER.warning(f"{len_error_str} 1") return None return struct.unpack("!b", tm_data[12:13])[0] - elif pfc == EcssPfcSigned.TWO_BYTES: + elif pfc == PfcSigned.TWO_BYTES: if param_len < 2: LOGGER.warning(f"{len_error_str} 2") return None return struct.unpack("!h", tm_data[12:14])[0] - elif pfc == EcssPfcSigned.FOUR_BYTES: + elif pfc == PfcSigned.FOUR_BYTES: if param_len < 4: LOGGER.warning(f"{len_error_str} 4") return None return struct.unpack("!i", tm_data[12:16])[0] - elif pfc == EcssPfcSigned.EIGHT_BYTES: + elif pfc == PfcSigned.EIGHT_BYTES: if param_len < 8: LOGGER.warning(f"{len_error_str} 8") return None @@ -243,13 +244,13 @@ def deserialize_scalar_entry(ptc: int, pfc: int, tm_data: bytes) -> Optional[any else: LOGGER.warning(f"Parsing of signed PTC {ptc} not implemented for PFC {pfc}") return None - if ptc == EcssPtc.REAL: - if pfc == EcssPfcReal.FLOAT_SIMPLE_PRECISION_IEEE: + if ptc == Ptc.REAL: + if pfc == PfcReal.FLOAT_SIMPLE_PRECISION_IEEE: if param_len < 4: LOGGER.warning(f"{len_error_str} 4") return None return struct.unpack("!f", tm_data[12:16])[0] - elif pfc == EcssPfcReal.DOUBLE_PRECISION_IEEE: + elif pfc == PfcReal.DOUBLE_PRECISION_IEEE: if param_len < 8: LOGGER.warning(f"{len_error_str} 8") return None diff --git a/src/tmtccmd/tm/pus_23_filemgmt.py b/tmtccmd/tm/pus_23_filemgmt.py similarity index 98% rename from src/tmtccmd/tm/pus_23_filemgmt.py rename to tmtccmd/tm/pus_23_filemgmt.py index 38fc9a39..ab38343e 100644 --- a/src/tmtccmd/tm/pus_23_filemgmt.py +++ b/tmtccmd/tm/pus_23_filemgmt.py @@ -1,7 +1,7 @@ from __future__ import annotations import struct -from spacepackets.ecss.definitions import PusServices +from spacepackets.ecss.defs import PusServices from spacepackets.ecss.tm import CdsShortTimestamp, PusVersion, PusTelemetry from tmtccmd.tm.base import PusTmInfoBase, PusTmBase @@ -53,7 +53,7 @@ def __init__( service=PusServices.SERVICE_23_FILE_MGMT, subservice=subservice_id, time=time, - ssc=ssc, + seq_count=ssc, source_data=source_data, apid=apid, packet_version=packet_version, diff --git a/src/tmtccmd/tm/pus_2_rawcmd.py b/tmtccmd/tm/pus_2_rawcmd.py similarity index 98% rename from src/tmtccmd/tm/pus_2_rawcmd.py rename to tmtccmd/tm/pus_2_rawcmd.py index 212668c8..9f02d540 100644 --- a/src/tmtccmd/tm/pus_2_rawcmd.py +++ b/tmtccmd/tm/pus_2_rawcmd.py @@ -24,7 +24,7 @@ def __init__( service=2, subservice=subservice, time=time, - ssc=ssc, + seq_count=ssc, source_data=source_data, apid=apid, packet_version=packet_version, diff --git a/src/tmtccmd/tm/pus_3_fsfw_hk.py b/tmtccmd/tm/pus_3_fsfw_hk.py similarity index 99% rename from src/tmtccmd/tm/pus_3_fsfw_hk.py rename to tmtccmd/tm/pus_3_fsfw_hk.py index dcd24497..fc83657b 100644 --- a/src/tmtccmd/tm/pus_3_fsfw_hk.py +++ b/tmtccmd/tm/pus_3_fsfw_hk.py @@ -64,7 +64,7 @@ def __init__( service=3, subservice=subservice_id, time=time, - ssc=ssc, + seq_count=ssc, source_data=source_data, apid=apid, packet_version=packet_version, diff --git a/src/tmtccmd/tm/pus_3_hk_base.py b/tmtccmd/tm/pus_3_hk_base.py similarity index 100% rename from src/tmtccmd/tm/pus_3_hk_base.py rename to tmtccmd/tm/pus_3_hk_base.py diff --git a/src/tmtccmd/tm/pus_5_event.py b/tmtccmd/tm/pus_5_event.py similarity index 98% rename from src/tmtccmd/tm/pus_5_event.py rename to tmtccmd/tm/pus_5_event.py index c68b2be0..57aaad0e 100644 --- a/src/tmtccmd/tm/pus_5_event.py +++ b/tmtccmd/tm/pus_5_event.py @@ -5,7 +5,7 @@ from abc import abstractmethod import struct -from spacepackets.ecss.definitions import PusServices +from spacepackets.ecss.defs import PusServices from spacepackets.ecss.pus_5_event import Subservices from spacepackets.ecss.tm import CdsShortTimestamp, PusVersion from tmtccmd.tm.base import PusTmInfoBase, PusTmBase, PusTelemetry @@ -56,7 +56,7 @@ def __init__( service=PusServices.S5_EVENT, subservice=subservice, time=time, - ssc=ssc, + seq_count=ssc, source_data=source_data, apid=apid, packet_version=packet_version, diff --git a/src/tmtccmd/tm/pus_8_funccmd.py b/tmtccmd/tm/pus_8_funccmd.py similarity index 99% rename from src/tmtccmd/tm/pus_8_funccmd.py rename to tmtccmd/tm/pus_8_funccmd.py index 4a5175c5..3955d79a 100644 --- a/src/tmtccmd/tm/pus_8_funccmd.py +++ b/tmtccmd/tm/pus_8_funccmd.py @@ -47,7 +47,7 @@ def __init__( service=5, subservice=subservice_id, time=time, - ssc=ssc, + seq_count=ssc, source_data=source_data, apid=apid, packet_version=packet_version, diff --git a/src/tmtccmd/utility/__init__.py b/tmtccmd/utility/__init__.py similarity index 100% rename from src/tmtccmd/utility/__init__.py rename to tmtccmd/utility/__init__.py diff --git a/src/tmtccmd/utility/conf_util.py b/tmtccmd/utility/conf_util.py similarity index 96% rename from src/tmtccmd/utility/conf_util.py rename to tmtccmd/utility/conf_util.py index 149b4291..9881e8dd 100644 --- a/src/tmtccmd/utility/conf_util.py +++ b/tmtccmd/utility/conf_util.py @@ -3,13 +3,16 @@ from contextlib import contextmanager from tmtccmd.core.globals_manager import get_global -from tmtccmd.config.definitions import CoreGlobalIds from tmtccmd.logging import get_console_logger LOGGER = get_console_logger() +def wrapped_prompt(text: str): + return input(text) + + class AnsiColors: RED = "\x1b[31m" GREEN = "\x1b[32m" @@ -18,6 +21,7 @@ class AnsiColors: MAGNETA = "\x1b[35m" CYAN = "\x1b[36m" RESET = "\x1b[0m" + BOLD = "\033[1m" def check_args_in_dict( @@ -88,6 +92,8 @@ def print_core_globals(): or as an optional information output :return: """ + from tmtccmd.config.globals import CoreGlobalIds + service_param = get_global(CoreGlobalIds.CURRENT_SERVICE) mode_param = get_global(CoreGlobalIds.MODE) com_if_param = get_global(CoreGlobalIds.COM_IF) diff --git a/tmtccmd/utility/countdown.py b/tmtccmd/utility/countdown.py new file mode 100644 index 00000000..38821b37 --- /dev/null +++ b/tmtccmd/utility/countdown.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import time +from typing import Optional +from datetime import timedelta + + +def time_ms() -> int: + return round(time.time() * 1000) + + +class Countdown: + def __init__(self, init_timeout_secs: Optional[timedelta]): + if init_timeout_secs is not None: + self._timeout_ms = int(init_timeout_secs / timedelta(milliseconds=1)) + self._start_time_ms = time_ms() + else: + self._timeout_ms = 0 + self._start_time_ms = 0 + + @classmethod + def from_millis(cls, timeout_ms: int) -> Countdown: + return cls(timedelta(milliseconds=timeout_ms)) + + @property + def timeout(self): + return self._timeout_ms + + @timeout.setter + def timeout(self, timeout: timedelta): + self._timeout_ms = round(timeout / timedelta(milliseconds=1)) + + def timed_out(self) -> bool: + if round(time_ms() - self._start_time_ms) >= self._timeout_ms: + return True + else: + return False + + def busy(self) -> bool: + return not self.timed_out() + + def reset(self, new_timeout: Optional[timedelta] = None): + if new_timeout is not None: + self.timeout = new_timeout + self.start() + + def start(self): + self._start_time_ms = time_ms() + + def time_out(self): + self._start_time_ms = 0 + + def rem_time(self) -> timedelta: + end_time = self._start_time_ms + self._timeout_ms + current = time_ms() + if end_time < current: + return timedelta() + return timedelta(milliseconds=end_time - current) diff --git a/src/tmtccmd/utility/exit_handler.py b/tmtccmd/utility/exit_handler.py similarity index 59% rename from src/tmtccmd/utility/exit_handler.py rename to tmtccmd/utility/exit_handler.py index e8ade115..2136f8b5 100644 --- a/src/tmtccmd/utility/exit_handler.py +++ b/tmtccmd/utility/exit_handler.py @@ -1,15 +1,13 @@ import signal -from tmtccmd.com_if.com_interface_base import CommunicationInterface -from tmtccmd.core.backend import TmTcHandler + +from tmtccmd.core import BackendBase from tmtccmd.logging import get_console_logger LOGGER = get_console_logger() -def keyboard_interrupt_handler( - tmtc_backend: TmTcHandler, com_interface: CommunicationInterface -): - tmtc_backend.close_listener(join=True, join_timeout_seconds=1.0) +def keyboard_interrupt_handler(tmtc_backend: BackendBase): + tmtc_backend.close_com_if() LOGGER.info("Closing TMTC client") diff --git a/src/tmtccmd/utility/hammingcode.py b/tmtccmd/utility/hammingcode.py similarity index 84% rename from src/tmtccmd/utility/hammingcode.py rename to tmtccmd/utility/hammingcode.py index c3df703d..dc60eabb 100644 --- a/src/tmtccmd/utility/hammingcode.py +++ b/tmtccmd/utility/hammingcode.py @@ -1,47 +1,42 @@ -""" -Hamming Code Implementation - +"""Hamming Code Implementation Hamming codes belong to the family of linear error correcting codes. Documentation: https://en.wikipedia.org/wiki/Hamming_code They can be used to identify up to two bit errors and correct one bit error per 256 byte block. """ - -""" -Translated from ATMEL C library. -/* ---------------------------------------------------------------------------- - * ATMEL Microcontroller Software Support - * ---------------------------------------------------------------------------- - * Copyright (c) 2008, Atmel Corporation - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * - Redistributions of source code must retain the above copyright notice, - * this list of conditions and the disclaimer below. - * - * Atmel's name may not be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * DISCLAIMER: THIS SOFTWARE IS PROVIDED BY ATMEL "AS IS" AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE - * DISCLAIMED. IN NO EVENT SHALL ATMEL BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * ---------------------------------------------------------------------------- - */ - -""" from enum import Enum - from tmtccmd.logging import get_console_logger +# Translated from ATMEL C library. +# /* ---------------------------------------------------------------------------- +# * ATMEL Microcontroller Software Support +# * ---------------------------------------------------------------------------- +# * Copyright (c) 2008, Atmel Corporation +# * +# * All rights reserved. +# * +# * Redistribution and use in source and binary forms, with or without +# * modification, are permitted provided that the following conditions are met: +# * +# * - Redistributions of source code must retain the above copyright notice, +# * this list of conditions and the disclaimer below. +# * +# * Atmel's name may not be used to endorse or promote products derived from +# * this software without specific prior written permission. +# * +# * DISCLAIMER: THIS SOFTWARE IS PROVIDED BY ATMEL "AS IS" AND ANY EXPRESS OR +# * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE +# * DISCLAIMED. IN NO EVENT SHALL ATMEL BE LIABLE FOR ANY DIRECT, INDIRECT, +# * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# * ---------------------------------------------------------------------------- +# */ + + LOGGER = get_console_logger() @@ -134,6 +129,7 @@ def hamming_verify_256x( def hamming_compute_256(data: bytearray) -> bytearray: """Takes a bytearray with the size of 256 bytes and calculates the 22 parity bits for the hamming code which will be returned as a three byte bytearray. + :param data: :return: """ @@ -247,14 +243,14 @@ def hamming_verify_256( """Verifies and corrects a 256-bytes block of data using the given 22-bits hamming code. Returns 0 if there is no error, otherwise returns a HAMMING_ERROR code. - :param data: 256 code block to verify, - :param original_hamming_code: Original 3 byte hamming code with 22 parity bits. + :param data: 256 code block to verify + :param original_hamming_code: Original 3 byte hamming code with 22 parity bits :return: See HammingReturnCodes enums. - -1 for invalid input. - 0 if there are no errors. - 1 if there is a single bit error which has been corrected. - 2 if the hamming code has been corrupted. - 3 if there was a multi bit error which can not be corrected. + - -1 for invalid input + - 0 if there are no errors. + - 1 if there is a single bit error which has been corrected + - 2 if the hamming code has been corrupted + - 3 if there was a multi bit error which can not be corrected """ if len(data) != 256: LOGGER.error( diff --git a/src/tmtccmd/utility/json_handler.py b/tmtccmd/utility/json.py similarity index 98% rename from src/tmtccmd/utility/json_handler.py rename to tmtccmd/utility/json.py index 442c209a..906edec8 100644 --- a/src/tmtccmd/utility/json_handler.py +++ b/tmtccmd/utility/json.py @@ -26,7 +26,8 @@ class JsonKeyNames(enum.Enum): def check_json_file(json_cfg_path: str) -> bool: """The check JSON file and return whether it was valid or not. A JSON file is invalid if it does not exist or the format ins invalid. - :return: True if JSON file is valid, False if not and a new one was created at the specified path + :return: True if JSON file is valid, False if not and a new one was created at the + specified path """ if json_cfg_path == "": json_cfg_path = "tmtc_config.json" diff --git a/src/tmtccmd/utility/obj_id.py b/tmtccmd/utility/obj_id.py similarity index 100% rename from src/tmtccmd/utility/obj_id.py rename to tmtccmd/utility/obj_id.py diff --git a/src/tmtccmd/utility/retval.py b/tmtccmd/utility/retval.py similarity index 100% rename from src/tmtccmd/utility/retval.py rename to tmtccmd/utility/retval.py diff --git a/src/tmtccmd/utility/tmtc_printer.py b/tmtccmd/utility/tmtc_printer.py similarity index 99% rename from src/tmtccmd/utility/tmtc_printer.py rename to tmtccmd/utility/tmtc_printer.py index e80186a4..b1214805 100644 --- a/src/tmtccmd/utility/tmtc_printer.py +++ b/tmtccmd/utility/tmtc_printer.py @@ -5,7 +5,7 @@ from typing import List, Optional from spacepackets.util import get_printable_data_string, PrintFormats -from spacepackets.ecss.definitions import PusServices +from spacepackets.ecss.defs import PusServices from tmtccmd.tm.pus_8_funccmd import Service8FsfwTm from tmtccmd.tm.base import PusTmInfoInterface, PusTmInterface