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 @@
+
+
+
+
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