diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4e54ada9..b17d586f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -41,6 +41,7 @@ jobs:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
+ token: ${{ secrets.CODECOV_TOKEN }}
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 05bc0ada..f47998ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,10 +9,19 @@ Starting from v4.0.0, this project adheres to [Semantic Versioning](http://semve
# [unreleased]
+# [v8.0.0rc1] 2024-01-24
+
## Fixed
- Fixed bug for acknowledged file transfer cancellation in the destination handler.
+## Removed
+
+- The `tmtccmd.util.countdown` and `tmtccmd.util.seqcnt` module were moved to the `spacepackets`
+ library.
+- Most of the `tmtccmd.cfdp` module was moved to the
+ [`cfdp-py` library](https://github.com/us-irs/cfdp-py).
+
# [v8.0.0rc0] 2023-11-29
## Added
diff --git a/README.md b/README.md
index ef1e5542..d2b59fbc 100644
--- a/README.md
+++ b/README.md
@@ -25,8 +25,6 @@ used either as a command line tool or as a GUI tool which requires a PyQt6 insta
packets and [CCSDS Space Packets](https://public.ccsds.org/Pubs/133x0b2e1.pdf).
This library uses the [spacepackets](https://github.com/us-irs/py-spacepackets) library for most
packet implementations.
-- High level CFDP components which allow to build
- [CFDP standard conformant](https://public.ccsds.org/Pubs/727x0b5.pdf) CFDP handlers.
- 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.
diff --git a/docs/api.rst b/docs/api.rst
index 953b2c82..075f1046 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -13,18 +13,7 @@ Communication Package
api/com
-CCSDS Package
-===================
-
-This currently also contains the CFDP support.
-
-.. toctree::
- :maxdepth: 2
-
- api/cfdp
- api/cfdp.handler
-
-ECSS Package
+ECSS & CCSDS Package
=========================
.. automodule:: tmtccmd.pus
@@ -35,6 +24,11 @@ ECSS Package
api/pus
+.. toctree::
+ :maxdepth: 2
+
+ api/cfdp
+
Application Package
=========================
diff --git a/docs/api/cfdp.handler.rst b/docs/api/cfdp.handler.rst
deleted file mode 100644
index 3cf574f3..00000000
--- a/docs/api/cfdp.handler.rst
+++ /dev/null
@@ -1,34 +0,0 @@
-CFDP Handler Package
-=============================
-
-Package Contents
------------------
-
-.. automodule:: tmtccmd.cfdp.handler
- :members:
- :undoc-members:
- :show-inheritance:
-
-Source Handler Module
--------------------------------------
-
-.. automodule:: tmtccmd.cfdp.handler.source
- :members:
- :undoc-members:
- :show-inheritance:
-
-Destination Handler Module
--------------------------------------
-
-.. automodule:: tmtccmd.cfdp.handler.dest
- :members:
- :undoc-members:
- :show-inheritance:
-
-Defintions Module
--------------------------------------
-
-.. automodule:: tmtccmd.cfdp.handler.defs
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/api/cfdp.rst b/docs/api/cfdp.rst
index d3d81c0a..4e320c26 100644
--- a/docs/api/cfdp.rst
+++ b/docs/api/cfdp.rst
@@ -1,60 +1,10 @@
-CFDP Package
-=======================
+CFDP Module
+==============
-Module contents
----------------
-
-.. automodule:: tmtccmd.cfdp
- :members:
- :undoc-members:
- :show-inheritance:
-
-Filestore Module
--------------------------------
-
-.. automodule:: tmtccmd.cfdp.filestore
- :members:
- :undoc-members:
- :show-inheritance:
-
-
-Management Information Base (MIB) Module
--------------------------------------------
-
-.. automodule:: tmtccmd.cfdp.mib
- :members:
- :undoc-members:
- :show-inheritance:
-
-Request Module
--------------------------------
+Request Submodule
+-----------------------------
.. automodule:: tmtccmd.cfdp.request
:members:
:undoc-members:
:show-inheritance:
-
-User Module
--------------------------------
-
-.. automodule:: tmtccmd.cfdp.user
- :members:
- :undoc-members:
- :show-inheritance:
-
-Exceptions Module
--------------------------------------------
-
-.. automodule:: tmtccmd.cfdp.exceptions
- :members:
- :undoc-members:
- :show-inheritance:
-
-Definitions Module
---------------------------
-
-.. automodule:: tmtccmd.cfdp.defs
- :members:
- :undoc-members:
- :show-inheritance:
-
diff --git a/docs/api/util.rst b/docs/api/util.rst
index fd36cdd6..23c71667 100644
--- a/docs/api/util.rst
+++ b/docs/api/util.rst
@@ -12,22 +12,6 @@ JSON Module
:undoc-members:
:show-inheritance:
-Countdown Module
-------------------
-
-.. automodule:: tmtccmd.util.countdown
- :members:
- :undoc-members:
- :show-inheritance:
-
-Sequence Count Module
---------------------------
-
-.. automodule:: tmtccmd.util.seqcnt
- :members:
- :undoc-members:
- :show-inheritance:
-
Object ID Module
----------------------
@@ -44,7 +28,7 @@ Exit Module
:undoc-members:
:show-inheritance:
-tmtccmd.utility.hammingcode module
+Hamming-Code Module
----------------------------------
.. automodule:: tmtccmd.util.hammingcode
@@ -52,15 +36,15 @@ tmtccmd.utility.hammingcode module
:undoc-members:
:show-inheritance:
-tmtccmd.utility.tmtc\_printer module
+TMTC Printer (FSFW) Module
------------------------------------
-.. automodule:: tmtccmd.util.tmtc_printer
+.. automodule:: tmtccmd.fsfw.tmtc_printer
:members:
:undoc-members:
:show-inheritance:
-tmtccmd.utility.conf\_util module
+Configuration Utility Module
---------------------------------
.. automodule:: tmtccmd.util.conf_util
@@ -75,3 +59,17 @@ Module contents
:members:
:undoc-members:
:show-inheritance:
+
+Countdown Module
+------------------
+
+The countdown module was moved to the `spacepackets` library. Use the
+:py:mod:`spacepackets.countdown` module.
+
+Sequence Count Module
+--------------------------
+
+The sequence count module was moved to the `spacepackets` library. Use the
+:py:mod:`spacepackets.seqcount` module.
+
+.. _`spacepackets`: https://github.com/us-irs/spacepackets-py
diff --git a/docs/cfdp.rst b/docs/cfdp.rst
index b3ff7815..5ea074f8 100644
--- a/docs/cfdp.rst
+++ b/docs/cfdp.rst
@@ -4,105 +4,7 @@
CCSDS File Delivery Protocol (CFDP)
====================================
-The ``tmtccmd`` package offers some high-level CCSDS File Delivery Protocol (CFDP) components to
-perform file transfers according to the `CCSDS Blue Book 727.0-B-5`_. The underlying base packet
-library used to generate the packets to be sent is the `spacepackets`_ library.
-The basic idea of CFDP is to convert files of any size into a stream of packets called packet
-data units (PDU). CFPD has an unacknowledged and acknowledged mode, with the option to request
-a transaction closure for the unacknowledged mode. Using the unacknowledged mode with no
-transaction closure is applicable for simplex communication paths, while the unacknowledged mode
-with closure is the easiest way to get a confirmation of a successfull file transfer, including a
-CRC check on the remote side to verify file integrity. The acknowledged mode is the most complex
-mode which includes multiple mechanism to ensure succesfull packet transaction even for unreliable
-connections, including lost segment detection. As such, it can be compared to a specialized TCP
-for file transfers with remote systems.
+The majority of the CFDP components were moved to the dedicated `cfdp-py`_ library.
+It is recommended to read its `dedicated documentation `_.
-The core of these high-level components are the :py:class:`tmtccmd.cfdp.handler.dest.DestHandler`
-and the :py:class:`tmtccmd.cfdp.handler.source.SourceHandler` component. These model the CFDP
-destination and CFDP source entity respectively.
-
-CFDP source entity
--------------------
-
-The :py:class:`tmtccmd.cfdp.handler.source.SourceHandler` converts a
-:py:class:`tmtccmd.cfdp.request.PutRequest` into all packet data units (PDUs) which need to be
-sent to a remote CFDP entity to perform a File Copy operation to a remote entity. The source entity
-allows freedom of communcation by only generating the packets required to be sent, and leaving the
-actual transmission of those packets to the user. The packet is returned to the user using
-the :py:class:`spacepackets.cfdp.pdu.helper.PduHolder` field of the
-:py:class:`tmtccmd.cfdp.handler.source.FsmResult` structure returned in the
-:py:meth:`tmtccmd.cfdp.handler.source.SourceHandler.state_machine` call.
-
-The state machine of the source entity will generally perform the following steps, after
-a valid :py:class:`tmtccmd.cfdp.request.PutRequest` to perform a File Copy operation was received:
-
-1. Generate the Metadata PDU to be sent to a remote CFDP entity. The PDU will be returned as a
- :py:class:`spacepackets.cfdp.pdu.metadata.MetadataPdu` instance.
-2. Generate all File Data PDUs to be sent to a remote CFDP entity, if applicable (file not empty).
- The PDU(s) will be returned as a :py:class:`spacepackets.cfdp.pdu.file_data.FileDataPdu`
- instance(s).
-3. Generate an EOF PDU be sent to a remote CFDP entity.
- The PDU will be returned as a :py:class:`spacepackets.cfdp.pdu.eof.EofPdu` instance.
-
-If this is an unacknowledged transfer with no transaction closure, the file transfer will be done
-after these steps. In any other case:
-
-**Unacknowledged transfer with requested closure**
-
-4. A Finished PDU will be awaited, for example one generated using :py:class:`spacepackets.cfdp.pdu.finished.FinishedPdu`.
-
-**Acknowledged transfer**
-
-4. A EOF ACK packet will be awaited, for example one generated using :py:class:`spacepackets.cfdp.pdu.ack.AckPdu`.
-5. A Finished PDU will be awaited, for example one generated using :py:class:`spacepackets.cfdp.pdu.finished.FinishedPdu`.
-6. A Finished PDU ACK packet will be sent to the remote CFDP entity.
-
-CFDP destination entity
-------------------------
-
-The :py:class:`tmtccmd.cfdp.handler.dest.DestHandler` can convert the PDUs sent from a remote
-source entity ID back to a file. A file copy operation on the receiver side is started with
-the reception of a Metadata PDU, for example one generated by the
-:py:class:`spacepackets.cfdp.pdu.metadata.MetadataPdu` class . After that, file packet PDUs, for
-example generated by the :py:class:`spacepackets.cfdp.pdu.file_data.FileDataPdu`, can be inserted
-into the destination handler and will be assembled into a file. The transaction will be finished
-for the following conditions:
-
-1. A valid EOF PDU, for example generated by the :py:class:`spacepackets.cfdp.pdu.eof.EofPdu`
- class, has been inserted into the class.
-2. All check timers have elapsed. These check timers allow and out-of-order reception of EOF and
- file data PDUs, provided that the interval between the EOF PDU and the last file data PDUs is
- not too large. Check timer support is not implemented yet.
-3. All confirmation packets like Finished PDUs or the EOF ACK PDU have been sent back and confirmed
- by the remote side where applicable.
-
-Current List of unimplemented features
-----------------------------------------
-
-The following features have not been implemented yet. PRs or notifications for demand are welcome!
-
-- Suspending transfers
-- Inactivity handling
-- Start and end of transmission and reception opportunity handling
-- Keep Alive and Prompt PDU handling
-
-Example application
---------------------
-
-You can find an example application inside the
-`example directory `_
-which shows an end-to-end file transfer on a host computer. This should give you a general idea of
-how the source and destination handler work in practice.
-
-There is also a
-`test application `_
-which cross-tests the `tmtccmd` CFDP implementation with the
-`Libre Cube CFDP `_ implementation.
-
-Finally, you can see a more complex example also featuring more features of the CFDP state machines
-`here `_. This example
-uses UDP servers for communication and explicitely separates the local and remote entity
-application.
-
-.. _`CCSDS Blue Book 727.0-B-5`: https://public.ccsds.org/Pubs/727x0b5.pdf
-.. _`spacepackets`: https://github.com/us-irs/spacepackets-py
+.. _cfdp-py: https://github.com/us-irs/cfdp-py
diff --git a/docs/conf.py b/docs/conf.py
index d9664dbc..3b9ca135 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -60,6 +60,7 @@
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"serial": ("https://pyserial.readthedocs.io/en/latest/", None),
+ "cfdp-py": ("https://cfdp-py.readthedocs.io/en/latest/", None),
"spacepackets": ("https://spacepackets.readthedocs.io/en/latest/", None),
"prompt-toolkit": ("https://python-prompt-toolkit.readthedocs.io/en/latest/", None),
}
diff --git a/docs/introduction.rst b/docs/introduction.rst
index 0a383d45..34c89aee 100644
--- a/docs/introduction.rst
+++ b/docs/introduction.rst
@@ -33,9 +33,6 @@ Features
more information and examples.
- Special support for `Packet Utilisation Standard (PUS)`_ packets and `CCSDS Space Packets`_.
This library uses the `spacepackets`_ library for most packet implementations.
-- High level CFDP components which allow to build `CFDP standard conformant`_ CFDP handlers.
- Currently only supports unacknowledged mode. The :ref:`cfdp` chapter contains more information
- and a link to an example application.
- 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.
diff --git a/examples/cfdp-cli-udp/.gitignore b/examples/cfdp-cli-udp/.gitignore
deleted file mode 100644
index b2272fa8..00000000
--- a/examples/cfdp-cli-udp/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/local_cfg.json
diff --git a/examples/cfdp-cli-udp/README.md b/examples/cfdp-cli-udp/README.md
deleted file mode 100644
index 9ec2a876..00000000
--- a/examples/cfdp-cli-udp/README.md
+++ /dev/null
@@ -1,56 +0,0 @@
-CFDP CLI Example with UDP servers
-=============
-
-This is a more complex example which also shows more features of the CFDP state machines.
-
-It has the following features:
-
-- Uses UDP as the underlying communication protocol
-- Remote and local entities are distinct application, which makes this a bit of a more realistic
- use case.
-- The local application exposes a minimal CLI interface to start both normal Put Requests and
- CFDP proxy put request to request files from the remote side.
-
-Here, the remote and local entities are distinct applications which both spawn UDP servers.
-The local entity will use the port 5111 while the remote entity will use the port 5222.
-This allows running both entities on the same computer.
-
-For example, you can run both `remote.py` and `local.py`. This will not do much because the local
-entity will not initiate a put request for this command.
-
-If you want to sent a file from the local application to the remote application, you can use
-the following commands:
-
-```sh
-echo "Hello World!" > files/local/hello.txt
-./local.py files/local/hello.txt files/remote
-```
-
-You can see the different indication steps for both the remote and local entity in the terminal.
-You can check that the file `files/remote/hello.txt` now exists with the correct content.
-
-After that, you can try a proxy put request to instruct the remote entity to send the file
-back to the local entity using the following command:
-
-```sh
-./local.py -p files/remote/hello.txt files/local/hello-sent-back.txt
-```
-
-You can also run the remote application on a different computer, for example a Raspberry Pi.
-Assuming that both computers are in the same network and the Raspberry Pi has the example address
-`192.168.55.55`, you can create a configuration file `local_cfg.json` with the following
-content:
-
-```sh
-{
- "remote_addr": "192.168.55.55"
-}
-```
-
-The local entity application will use this address instead of the localhost address.
-
-## Notes on example structure
-
-This application also shows a possible modular approach with re-usability in mind. Both the
-source, destination entity and the UDP server are based on the same classes. The configuration
-of the CFDP is also in large parts re-used and provided inside the `common` module.
diff --git a/examples/cfdp-cli-udp/common.py b/examples/cfdp-cli-udp/common.py
deleted file mode 100644
index 7c962cb4..00000000
--- a/examples/cfdp-cli-udp/common.py
+++ /dev/null
@@ -1,522 +0,0 @@
-from typing import Any, Tuple, Optional, List, Dict
-from multiprocessing import Queue
-from queue import Empty
-from threading import Thread
-import time
-import select
-import socket
-import logging
-import copy
-import json
-
-from datetime import timedelta
-from spacepackets.cfdp import GenericPduPacket
-from pathlib import Path
-from spacepackets.cfdp.pdu import AbstractFileDirectiveBase
-from spacepackets.cfdp import (
- TransactionId,
- ConditionCode,
- TransmissionMode,
- ChecksumType,
-)
-from spacepackets.cfdp.tlv import (
- ProxyMessageType,
- ProxyPutResponse,
- ProxyPutResponseParams,
- MessageToUserTlv,
- OriginatingTransactionId,
- ReservedCfdpMessage,
-)
-from spacepackets.util import UnsignedByteField, ByteFieldU16
-from tmtccmd.cfdp.user import (
- CfdpUserBase,
- FileSegmentRecvdParams,
- TransactionParams,
- MetadataRecvParams,
- TransactionFinishedParams,
-)
-from tmtccmd.cfdp.exceptions import InvalidDestinationId, SourceFileDoesNotExist
-
-from tmtccmd.cfdp import get_packet_destination, PacketDestination
-from tmtccmd.util.countdown import Countdown
-from tmtccmd.cfdp.mib import (
- CheckTimerProvider,
- DefaultFaultHandlerBase,
- EntityType,
- IndicationCfg,
- RemoteEntityCfg,
-)
-from tmtccmd.cfdp.handler import SourceHandler, DestHandler, CfdpState
-from tmtccmd.cfdp import PutRequest
-from spacepackets.cfdp.pdu import PduFactory, PduHolder
-
-_LOGGER = logging.getLogger(__name__)
-
-LOCAL_ENTITY_ID = ByteFieldU16(1)
-REMOTE_ENTITY_ID = ByteFieldU16(2)
-# Enable all indications for both local and remote entity.
-INDICATION_CFG = IndicationCfg()
-
-FILE_CONTENT = "Hello World!\n"
-FILE_SEGMENT_SIZE = 256
-MAX_PACKET_LEN = 512
-
-REMOTE_CFG_OF_LOCAL_ENTITY = RemoteEntityCfg(
- entity_id=LOCAL_ENTITY_ID,
- max_packet_len=MAX_PACKET_LEN,
- max_file_segment_len=FILE_SEGMENT_SIZE,
- closure_requested=True,
- crc_on_transmission=False,
- default_transmission_mode=TransmissionMode.ACKNOWLEDGED,
- crc_type=ChecksumType.CRC_32,
-)
-
-REMOTE_CFG_OF_REMOTE_ENTITY = copy.copy(REMOTE_CFG_OF_LOCAL_ENTITY)
-REMOTE_CFG_OF_REMOTE_ENTITY.entity_id = REMOTE_ENTITY_ID
-
-LOCAL_PORT = 5111
-REMOTE_PORT = 5222
-
-
-class CfdpFaultHandler(DefaultFaultHandlerBase):
- def __init__(self, base_str: str):
- self.base_str = base_str
- super().__init__()
-
- def notice_of_suspension_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- _LOGGER.warning(
- f"{self.base_str}: Received Notice of Suspension for transaction {transaction_id!r} "
- f"with condition code {cond!r}. Progress: {progress}"
- )
-
- def notice_of_cancellation_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- _LOGGER.warning(
- f"{self.base_str}: Received Notice of Cancellation for transaction {transaction_id!r} "
- f"with condition code {cond!r}. Progress: {progress}"
- )
-
- def abandoned_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- _LOGGER.warning(
- f"{self.base_str}: Abandoned fault for transaction {transaction_id!r} "
- f"with condition code {cond!r}. Progress: {progress}"
- )
-
- def ignore_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- _LOGGER.warning(
- f"{self.base_str}: Ignored fault for transaction {transaction_id!r} "
- f"with condition code {cond!r}. Progress: {progress}"
- )
-
-
-class CfdpUser(CfdpUserBase):
- def __init__(self, base_str: str, put_req_queue: Queue):
- self.base_str = base_str
- self.put_req_queue = put_req_queue
- # This is a dictionary where the key is the current transaction ID for a transaction which
- # was triggered by a proxy request with a originating ID.
- self.active_proxy_put_reqs: Dict[TransactionId, TransactionId] = {}
- super().__init__()
-
- def transaction_indication(
- self,
- transaction_params: TransactionParams,
- ):
- """This indication is used to report the transaction ID to the CFDP user"""
- _LOGGER.info(
- f"{self.base_str}: Transaction.indication for {transaction_params.transaction_id}"
- )
- if transaction_params.originating_transaction_id is not None:
- _LOGGER.info(
- f"Originating Transaction ID: {transaction_params.originating_transaction_id}"
- )
- self.active_proxy_put_reqs.update(
- {
- transaction_params.transaction_id: transaction_params.originating_transaction_id
- }
- )
-
- def eof_sent_indication(self, transaction_id: TransactionId):
- _LOGGER.info(f"{self.base_str}: EOF-Sent.indication for {transaction_id}")
-
- def transaction_finished_indication(self, params: TransactionFinishedParams):
- _LOGGER.info(
- f"{self.base_str}: Transaction-Finished.indication for {params.transaction_id}."
- )
- _LOGGER.info(f"Condition Code: {params.finished_params.condition_code!r}")
- _LOGGER.info(f"Delivery Code: {params.finished_params.delivery_code!r}")
- _LOGGER.info(f"File Status: {params.finished_params.file_status!r}")
- if params.transaction_id in self.active_proxy_put_reqs:
- proxy_put_response = ProxyPutResponse(
- ProxyPutResponseParams.from_finished_params(params.finished_params)
- ).to_generic_msg_to_user_tlv()
- originating_id = self.active_proxy_put_reqs.get(params.transaction_id)
- assert originating_id is not None
- put_req = PutRequest(
- destination_id=originating_id.source_id,
- source_file=None,
- dest_file=None,
- trans_mode=None,
- closure_requested=None,
- msgs_to_user=[
- proxy_put_response,
- OriginatingTransactionId(
- originating_id
- ).to_generic_msg_to_user_tlv(),
- ],
- )
- _LOGGER.info(
- f"Requesting Proxy Put Response concluding Proxy Put originating from "
- f"{originating_id}"
- )
- self.put_req_queue.put(put_req)
- self.active_proxy_put_reqs.pop(params.transaction_id)
-
- def metadata_recv_indication(self, params: MetadataRecvParams):
- _LOGGER.info(
- f"{self.base_str}: Metadata-Recv.indication for {params.transaction_id}."
- )
- if params.msgs_to_user is not None:
- self._handle_msgs_to_user(params.transaction_id, params.msgs_to_user)
-
- def _handle_msgs_to_user(
- self, transaction_id: TransactionId, msgs_to_user: List[MessageToUserTlv]
- ):
- for msg_to_user in msgs_to_user:
- if msg_to_user.is_reserved_cfdp_message():
- reserved_msg_tlv = msg_to_user.to_reserved_msg_tlv()
- assert reserved_msg_tlv is not None
- self._handle_reserved_cfdp_message(transaction_id, reserved_msg_tlv)
- else:
- _LOGGER.info(f"Received custom message to user: {msg_to_user}")
-
- def _handle_reserved_cfdp_message(
- self, transaction_id: TransactionId, reserved_cfdp_msg: ReservedCfdpMessage
- ):
- if reserved_cfdp_msg.is_cfdp_proxy_operation():
- self._handle_cfdp_proxy_operation(transaction_id, reserved_cfdp_msg)
- elif reserved_cfdp_msg.is_originating_transaction_id():
- _LOGGER.info(
- f"Received originating transaction ID: "
- f"{reserved_cfdp_msg.get_originating_transaction_id()}"
- )
-
- def _handle_cfdp_proxy_operation(
- self, transaction_id: TransactionId, reserved_cfdp_msg: ReservedCfdpMessage
- ):
- if (
- reserved_cfdp_msg.get_cfdp_proxy_message_type()
- == ProxyMessageType.PUT_REQUEST
- ):
- put_req_params = reserved_cfdp_msg.get_proxy_put_request_params()
- _LOGGER.info(f"Received Proxy Put Request: {put_req_params}")
- assert put_req_params is not None
- put_req = PutRequest(
- destination_id=put_req_params.dest_entity_id,
- source_file=put_req_params.source_file_as_path,
- dest_file=put_req_params.dest_file_as_path,
- trans_mode=None,
- closure_requested=None,
- msgs_to_user=[
- OriginatingTransactionId(
- transaction_id
- ).to_generic_msg_to_user_tlv()
- ],
- )
- self.put_req_queue.put(put_req)
- elif (
- reserved_cfdp_msg.get_cfdp_proxy_message_type()
- == ProxyMessageType.PUT_RESPONSE
- ):
- put_response_params = reserved_cfdp_msg.get_proxy_put_response_params()
- _LOGGER.info(f"Received Proxy Put Response: {put_response_params}")
-
- def file_segment_recv_indication(self, params: FileSegmentRecvdParams):
- _LOGGER.info(
- f"{self.base_str}: File-Segment-Recv.indication for {params.transaction_id}."
- )
-
- def report_indication(self, transaction_id: TransactionId, status_report: Any):
- # TODO: p.28 of the CFDP standard specifies what information the status report parameter
- # could contain. I think it would be better to not hardcode the type of the status
- # report here, but something like Union[any, CfdpStatusReport] with CfdpStatusReport
- # being an implementation which supports all three information suggestions would be
- # nice
- pass
-
- def suspended_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode
- ):
- _LOGGER.info(
- f"{self.base_str}: Suspended.indication for {transaction_id} | Condition Code: {cond_code}"
- )
-
- def resumed_indication(self, transaction_id: TransactionId, progress: int):
- _LOGGER.info(
- f"{self.base_str}: Resumed.indication for {transaction_id} | Progress: {progress} bytes"
- )
-
- def fault_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode, progress: int
- ):
- _LOGGER.info(
- f"{self.base_str}: Fault.indication for {transaction_id} | Condition Code: {cond_code} | "
- f"Progress: {progress} bytes"
- )
-
- def abandoned_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode, progress: int
- ):
- _LOGGER.info(
- f"{self.base_str}: Abandoned.indication for {transaction_id} | Condition Code: {cond_code} |"
- f" Progress: {progress} bytes"
- )
-
- def eof_recv_indication(self, transaction_id: TransactionId):
- _LOGGER.info(f"{self.base_str}: EOF-Recv.indication for {transaction_id}")
-
-
-class CustomCheckTimerProvider(CheckTimerProvider):
- def provide_check_timer(
- self,
- local_entity_id: UnsignedByteField,
- remote_entity_id: UnsignedByteField,
- entity_type: EntityType,
- ) -> Countdown:
- return Countdown(timedelta(seconds=5.0))
-
-
-class UdpServer(Thread):
- def __init__(
- self,
- sleep_time: float,
- addr: Tuple[str, int],
- explicit_remote_addr: Optional[Tuple[str, int]],
- tx_queue: Queue,
- source_entity_rx_queue: Queue,
- dest_entity_rx_queue: Queue,
- ):
- super().__init__()
- self.sleep_time = sleep_time
- self.udp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
- self.addr = addr
- self.explicit_remote_addr = explicit_remote_addr
- self.udp_socket.bind(addr)
- self.tm_queue = tx_queue
- self.last_sender = None
- self.source_entity_queue = source_entity_rx_queue
- self.dest_entity_queue = dest_entity_rx_queue
-
- def run(self):
- _LOGGER.info(f"Starting UDP server on {self.addr}")
- while True:
- self.periodic_operation()
- time.sleep(self.sleep_time)
-
- def periodic_operation(self):
- while True:
- next_packet = self.poll_next_udp_packet()
- if next_packet is None or next_packet.pdu is None:
- break
- # Perform PDU routing.
- packet_dest = get_packet_destination(next_packet.pdu)
- if packet_dest == PacketDestination.DEST_HANDLER:
- self.dest_entity_queue.put(next_packet.pdu)
- elif packet_dest == PacketDestination.SOURCE_HANDLER:
- self.source_entity_queue.put(next_packet.pdu)
- self.send_packets()
-
- def poll_next_udp_packet(self) -> Optional[PduHolder]:
- ready = select.select([self.udp_socket], [], [], 0)
- if ready[0]:
- data, self.last_sender = self.udp_socket.recvfrom(4096)
- return PduFactory.from_raw_to_holder(data)
- return None
-
- def send_packets(self):
- while True:
- try:
- next_tm = self.tm_queue.get(False)
- if not isinstance(next_tm, bytes) and not isinstance(
- next_tm, bytearray
- ):
- _LOGGER.error(
- f"UDP server can only sent bytearray, received {next_tm}"
- )
- continue
- if self.explicit_remote_addr is not None:
- self.udp_socket.sendto(next_tm, self.explicit_remote_addr)
- elif self.last_sender is not None:
- self.udp_socket.sendto(next_tm, self.last_sender)
- else:
- _LOGGER.warning(
- "UDP Server: No packet destination found, dropping TM"
- )
- except Empty:
- break
-
-
-class SourceEntityHandler(Thread):
- def __init__(
- self,
- base_str: str,
- verbose_level: int,
- source_handler: SourceHandler,
- put_req_queue: Queue,
- source_entity_queue: Queue,
- tm_queue: Queue,
- ):
- super().__init__()
- self.base_str = base_str
- self.verbose_level = verbose_level
- self.source_handler = source_handler
- self.put_req_queue = put_req_queue
- self.source_entity_queue = source_entity_queue
- self.tm_queue = tm_queue
-
- def _idle_handling(self) -> bool:
- try:
- put_req: PutRequest = self.put_req_queue.get(False)
- _LOGGER.info(f"{self.base_str}: Handling Put Request: {put_req}")
- if put_req.destination_id not in [LOCAL_ENTITY_ID, REMOTE_ENTITY_ID]:
- _LOGGER.warning(
- f"can only handle put requests target towards {REMOTE_ENTITY_ID} or "
- f"{LOCAL_ENTITY_ID}"
- )
- else:
- try:
- self.source_handler.put_request(put_req)
- except SourceFileDoesNotExist as e:
- _LOGGER.warning(
- f"can not handle put request, source file {e.file} does not exist"
- )
- return True
- except Empty:
- return False
-
- def _busy_handling(self):
- # We are getting the packets from a Queue here, they could for example also be polled
- # from a network.
- no_packet_received = True
- try:
- # We are getting the packets from a Queue here, they could for example also be polled
- # from a network.
- packet: AbstractFileDirectiveBase = self.source_entity_queue.get(False)
- try:
- self.source_handler.insert_packet(packet)
- except InvalidDestinationId as e:
- _LOGGER.warning(
- f"invalid destination ID {e.found_dest_id} on packet {packet}, expected "
- f"{e.expected_dest_id}"
- )
- no_packet_received = False
- except Empty:
- no_packet_received = True
- try:
- no_packet_sent = self._call_source_state_machine()
- # If there is no work to do, put the thread to sleep.
- if no_packet_received and no_packet_sent:
- return False
- except SourceFileDoesNotExist:
- _LOGGER.warning("Source file does not exist")
- self.source_handler.reset()
-
- def _call_source_state_machine(self) -> bool:
- """Returns whether a packet was sent."""
- fsm_result = self.source_handler.state_machine()
- if fsm_result.states.num_packets_ready > 0:
- while fsm_result.states.num_packets_ready > 0:
- next_pdu_wrapper = self.source_handler.get_next_packet()
- assert next_pdu_wrapper is not None
- if self.verbose_level >= 1:
- _LOGGER.debug(
- f"{self.base_str}: Sending packet {next_pdu_wrapper.pdu}"
- )
- # Send all packets which need to be sent.
- self.tm_queue.put(next_pdu_wrapper.pack())
- return False
- else:
- return True
-
- def run(self):
- _LOGGER.info(f"Starting {self.base_str}")
- while True:
- if self.source_handler.state == CfdpState.IDLE:
- if not self._idle_handling():
- time.sleep(0.2)
- continue
- if self.source_handler.state == CfdpState.BUSY:
- if not self._busy_handling():
- time.sleep(0.2)
-
-
-class DestEntityHandler(Thread):
- def __init__(
- self,
- base_str: str,
- verbose_level: int,
- dest_handler: DestHandler,
- dest_entity_queue: Queue,
- tm_queue: Queue,
- ):
- super().__init__()
- self.base_str = base_str
- self.verbose_level = verbose_level
- self.dest_handler = dest_handler
- self.dest_entity_queue = dest_entity_queue
- self.tm_queue = tm_queue
-
- def run(self):
- _LOGGER.info(
- f"Starting {self.base_str}. Local ID {self.dest_handler.cfg.local_entity_id}"
- )
- first_packet = True
- no_packet_received = False
- while True:
- try:
- packet: GenericPduPacket = self.dest_entity_queue.get(False)
- self.dest_handler.insert_packet(packet)
- no_packet_received = False
- if first_packet:
- first_packet = False
- except Empty:
- no_packet_received = True
- fsm_result = self.dest_handler.state_machine()
- if fsm_result.states.num_packets_ready > 0:
- no_packet_sent = False
- while fsm_result.states.num_packets_ready > 0:
- next_pdu_wrapper = self.dest_handler.get_next_packet()
- assert next_pdu_wrapper is not None
- if self.verbose_level >= 1:
- _LOGGER.debug(
- f"REMOTE DEST ENTITY: Sending packet {next_pdu_wrapper.pdu}"
- )
- self.tm_queue.put(next_pdu_wrapper.pack())
- else:
- no_packet_sent = True
- # If there is no work to do, put the thread to sleep.
- if no_packet_received and no_packet_sent:
- time.sleep(0.5)
-
-
-def parse_remote_addr_from_json(file_path: Path) -> Optional[str]:
- try:
- with open(file_path, "r") as file:
- data = json.load(file)
- remote_addr = data.get("remote_addr")
- return remote_addr
- except FileNotFoundError:
- return None
- except json.JSONDecodeError:
- _LOGGER.warning(f"Error decoding JSON in {file_path}. Check the file format.")
- return None
- except KeyError:
- print("The 'remote_addr' key is missing in the JSON file.")
- return None
diff --git a/examples/cfdp-cli-udp/files/local/.gitignore b/examples/cfdp-cli-udp/files/local/.gitignore
deleted file mode 100644
index 120f485d..00000000
--- a/examples/cfdp-cli-udp/files/local/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!/.gitignore
diff --git a/examples/cfdp-cli-udp/files/remote/.gitignore b/examples/cfdp-cli-udp/files/remote/.gitignore
deleted file mode 100644
index 120f485d..00000000
--- a/examples/cfdp-cli-udp/files/remote/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!/.gitignore
diff --git a/examples/cfdp-cli-udp/local.py b/examples/cfdp-cli-udp/local.py
deleted file mode 100755
index 82d13541..00000000
--- a/examples/cfdp-cli-udp/local.py
+++ /dev/null
@@ -1,151 +0,0 @@
-#!/usr/bin/env python3
-"""This component simulates the local component."""
-import argparse
-import logging
-import ipaddress
-from logging import basicConfig
-from multiprocessing import Queue
-from pathlib import Path
-
-
-from common import (
- INDICATION_CFG,
- LOCAL_ENTITY_ID,
- REMOTE_ENTITY_ID,
- REMOTE_CFG_OF_REMOTE_ENTITY,
- LOCAL_PORT,
- REMOTE_PORT,
- CfdpFaultHandler,
- CfdpUser,
- CustomCheckTimerProvider,
- DestEntityHandler,
- SourceEntityHandler,
- UdpServer,
- parse_remote_addr_from_json,
-)
-
-from tmtccmd.cfdp.handler.dest import DestHandler
-from tmtccmd.cfdp.handler.source import SourceHandler
-from tmtccmd.cfdp.mib import (
- LocalEntityCfg,
- RemoteEntityCfgTable,
-)
-from tmtccmd.config.args import (
- add_cfdp_procedure_arguments,
- cfdp_args_to_cfdp_params,
- CfdpParams,
-)
-from tmtccmd.config.cfdp import generic_cfdp_params_to_put_request
-from tmtccmd.util.seqcnt import SeqCountProvider
-
-_LOGGER = logging.getLogger(__name__)
-
-BASE_STR_SRC = "LOCAL SRC"
-BASE_STR_DEST = "LOCAL DEST"
-LOCAL_CFG_JSON_PATH = "local_cfg.json"
-
-# This queue is used to send put requests.
-PUT_REQ_QUEUE = Queue()
-# All telecommands which should go to the source handler should be put into this queue by
-# the UDP server.
-SOURCE_ENTITY_QUEUE = Queue()
-# All telecommands which should go to the destination handler should be put into this queue by
-# the UDP server.
-DEST_ENTITY_QUEUE = Queue()
-# All telemetry which should be sent to the remote entity is put into this queue and will then
-# be sent by the UDP server.
-TM_QUEUE = Queue()
-
-
-def main():
- parser = argparse.ArgumentParser(prog="CFDP Local Entity Application")
- parser.add_argument("-v", "--verbose", action="count", default=0)
- add_cfdp_procedure_arguments(parser)
- args = parser.parse_args()
- logging_level = logging.INFO
- if args.verbose >= 1:
- logging_level = logging.DEBUG
- if args.source is not None and args.target is not None:
- # Generate a put request from the CLI arguments.
- cfdp_params = CfdpParams()
- cfdp_args_to_cfdp_params(args, cfdp_params)
- put_req = generic_cfdp_params_to_put_request(
- cfdp_params, LOCAL_ENTITY_ID, REMOTE_ENTITY_ID, LOCAL_ENTITY_ID
- )
- PUT_REQ_QUEUE.put(put_req)
-
- basicConfig(level=logging_level)
-
- remote_cfg_table = RemoteEntityCfgTable()
- remote_cfg_table.add_config(REMOTE_CFG_OF_REMOTE_ENTITY)
-
- src_fault_handler = CfdpFaultHandler(BASE_STR_SRC)
- # 16 bit sequence count for transactions.
- src_seq_count_provider = SeqCountProvider(16)
- src_user = CfdpUser(BASE_STR_SRC, PUT_REQ_QUEUE)
- check_timer_provider = CustomCheckTimerProvider()
- source_handler = SourceHandler(
- cfg=LocalEntityCfg(LOCAL_ENTITY_ID, INDICATION_CFG, src_fault_handler),
- seq_num_provider=src_seq_count_provider,
- remote_cfg_table=remote_cfg_table,
- user=src_user,
- check_timer_provider=check_timer_provider,
- )
- source_entity_task = SourceEntityHandler(
- BASE_STR_SRC,
- logging_level,
- source_handler,
- PUT_REQ_QUEUE,
- SOURCE_ENTITY_QUEUE,
- TM_QUEUE,
- )
-
- # Enable all indications.
- dest_fault_handler = CfdpFaultHandler(BASE_STR_DEST)
- dest_user = CfdpUser(BASE_STR_DEST, PUT_REQ_QUEUE)
- dest_handler = DestHandler(
- cfg=LocalEntityCfg(LOCAL_ENTITY_ID, INDICATION_CFG, dest_fault_handler),
- user=dest_user,
- remote_cfg_table=remote_cfg_table,
- check_timer_provider=check_timer_provider,
- )
- dest_entity_task = DestEntityHandler(
- BASE_STR_DEST,
- logging_level,
- dest_handler,
- DEST_ENTITY_QUEUE,
- TM_QUEUE,
- )
-
- # Address Any to accept CFDP packets from other address than localhost.
- local_addr = ipaddress.ip_address("0.0.0.0")
- # Localhost as default.
- remote_addr = ipaddress.ip_address("127.0.0.1")
- if Path(LOCAL_CFG_JSON_PATH).exists():
- addr_from_cfg = parse_remote_addr_from_json(Path(LOCAL_CFG_JSON_PATH))
- if addr_from_cfg is not None:
- try:
- remote_addr = ipaddress.ip_address(addr_from_cfg)
- except ValueError:
- _LOGGER.warning(f"invalid remote address {remote_addr} from JSON file")
- _LOGGER.info(f"Put request will be sent to remote destination {remote_addr}")
- udp_server = UdpServer(
- sleep_time=0.1,
- addr=(str(local_addr), LOCAL_PORT),
- explicit_remote_addr=(str(remote_addr), REMOTE_PORT),
- tx_queue=TM_QUEUE,
- source_entity_rx_queue=SOURCE_ENTITY_QUEUE,
- dest_entity_rx_queue=DEST_ENTITY_QUEUE,
- )
-
- source_entity_task.start()
- dest_entity_task.start()
- udp_server.start()
-
- source_entity_task.join()
- dest_entity_task.join()
- udp_server.join()
-
-
-if __name__ == "__main__":
- main()
diff --git a/examples/cfdp-cli-udp/remote.py b/examples/cfdp-cli-udp/remote.py
deleted file mode 100755
index 55d15f50..00000000
--- a/examples/cfdp-cli-udp/remote.py
+++ /dev/null
@@ -1,118 +0,0 @@
-#!/usr/bin/env python3
-"""This component simulates the remote component."""
-import argparse
-import logging
-from logging import basicConfig
-from multiprocessing import Queue
-
-from common import (
- INDICATION_CFG,
- REMOTE_ENTITY_ID,
- REMOTE_PORT,
- REMOTE_CFG_OF_LOCAL_ENTITY,
- CfdpFaultHandler,
- CfdpUser,
- CustomCheckTimerProvider,
- DestEntityHandler,
- SourceEntityHandler,
- UdpServer,
-)
-
-from tmtccmd.cfdp.handler.dest import DestHandler
-from tmtccmd.cfdp.handler.source import SourceHandler
-from tmtccmd.cfdp.mib import (
- LocalEntityCfg,
- RemoteEntityCfgTable,
-)
-from tmtccmd.util.seqcnt import SeqCountProvider
-
-_LOGGER = logging.getLogger(__name__)
-
-
-BASE_STR_SRC = "REMOTE SRC"
-BASE_STR_DEST = "REMOTE DEST"
-
-# This queue is used to send put requests.
-PUT_REQ_QUEUE = Queue()
-# All telecommands which should go to the source handler should be put into this queue by
-# the UDP server.
-SOURCE_ENTITY_QUEUE = Queue()
-# All telecommands which should go to the destination handler should be put into this queue by
-# the UDP server.
-DEST_ENTITY_QUEUE = Queue()
-# All telemetry which should be sent to the local entity is put into this queue and will then
-# be sent by the UDP server.
-TM_QUEUE = Queue()
-
-
-def main():
- parser = argparse.ArgumentParser(prog="CFDP Remote Entity Application")
- parser.add_argument("-v", "--verbose", action="count", default=0)
- args = parser.parse_args()
- logging_level = logging.INFO
- if args.verbose >= 1:
- logging_level = logging.DEBUG
- basicConfig(level=logging_level)
-
- src_fault_handler = CfdpFaultHandler(BASE_STR_SRC)
- # 16 bit sequence count for transactions.
- src_seq_count_provider = SeqCountProvider(16)
- src_user = CfdpUser(BASE_STR_SRC, PUT_REQ_QUEUE)
- remote_cfg_table = RemoteEntityCfgTable()
- remote_cfg_table.add_config(REMOTE_CFG_OF_LOCAL_ENTITY)
- check_timer_provider = CustomCheckTimerProvider()
- source_handler = SourceHandler(
- cfg=LocalEntityCfg(REMOTE_ENTITY_ID, INDICATION_CFG, src_fault_handler),
- user=src_user,
- remote_cfg_table=remote_cfg_table,
- check_timer_provider=check_timer_provider,
- seq_num_provider=src_seq_count_provider,
- )
- source_entity_task = SourceEntityHandler(
- BASE_STR_SRC,
- logging_level,
- source_handler,
- PUT_REQ_QUEUE,
- SOURCE_ENTITY_QUEUE,
- TM_QUEUE,
- )
-
- # Enable all indications.
- dest_fault_handler = CfdpFaultHandler(BASE_STR_DEST)
- dest_user = CfdpUser(BASE_STR_DEST, PUT_REQ_QUEUE)
- dest_handler = DestHandler(
- cfg=LocalEntityCfg(REMOTE_ENTITY_ID, INDICATION_CFG, dest_fault_handler),
- user=dest_user,
- remote_cfg_table=remote_cfg_table,
- check_timer_provider=check_timer_provider,
- )
- dest_entity_task = DestEntityHandler(
- BASE_STR_DEST,
- logging_level,
- dest_handler,
- DEST_ENTITY_QUEUE,
- TM_QUEUE,
- )
-
- # Address Any to accept CFDP packets from other address than localhost.
- local_addr = "0.0.0.0"
- udp_server = UdpServer(
- sleep_time=0.1,
- addr=(local_addr, REMOTE_PORT),
- # No explicit remote address, remote server only responds to requests.
- explicit_remote_addr=None,
- tx_queue=TM_QUEUE,
- source_entity_rx_queue=SOURCE_ENTITY_QUEUE,
- dest_entity_rx_queue=DEST_ENTITY_QUEUE,
- )
-
- source_entity_task.start()
- dest_entity_task.start()
- udp_server.start()
- source_entity_task.join()
- dest_entity_task.join()
- udp_server.join()
-
-
-if __name__ == "__main__":
- main()
diff --git a/examples/cfdp-libre-cube-crosstest/README.md b/examples/cfdp-libre-cube-crosstest/README.md
deleted file mode 100644
index 09787ea3..00000000
--- a/examples/cfdp-libre-cube-crosstest/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-CFDP Libre Cube Cross Test
-=========
-
-1. Install the LibreCube `cfdp` dependency first: `pip install -r requirements.txt`
-2. The `libre-cube-server.py` runs as the remote entity, so start it first.
-3. Now you can run `tmtccmd-client.py` to perform a file transfer. See `tmtccmd-client.py -h` for
- more information.
diff --git a/examples/cfdp-libre-cube-crosstest/common.py b/examples/cfdp-libre-cube-crosstest/common.py
deleted file mode 100755
index 8514a0dd..00000000
--- a/examples/cfdp-libre-cube-crosstest/common.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from spacepackets.cfdp import TransmissionMode, ChecksumType
-from spacepackets.util import ByteFieldU16
-from tmtccmd.cfdp.mib import RemoteEntityCfg
-
-SOURCE_ENTITY_ID = 2
-REMOTE_ENTITY_ID = 3
-
-UDP_TM_SERVER_PORT = 5111
-UDP_SERVER_PORT = 5222
-
-FILE_SEGMENT_SIZE = 128
-MAX_PACKET_LEN = 512
-
-REMOTE_CFG_FOR_DEST_ENTITY = RemoteEntityCfg(
- entity_id=ByteFieldU16(REMOTE_ENTITY_ID),
- max_packet_len=MAX_PACKET_LEN,
- max_file_segment_len=FILE_SEGMENT_SIZE,
- closure_requested=True,
- crc_on_transmission=False,
- default_transmission_mode=TransmissionMode.ACKNOWLEDGED,
- crc_type=ChecksumType.MODULAR,
-)
diff --git a/examples/cfdp-libre-cube-crosstest/libre-cube-server.py b/examples/cfdp-libre-cube-crosstest/libre-cube-server.py
deleted file mode 100755
index 273e17d6..00000000
--- a/examples/cfdp-libre-cube-crosstest/libre-cube-server.py
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env python3
-import logging
-
-import cfdp
-from cfdp.transport.udp import UdpTransport
-from cfdp.filestore import NativeFileStore
-from common import REMOTE_ENTITY_ID, UDP_SERVER_PORT
-
-
-logging.basicConfig(level=logging.DEBUG)
-
-udp_transport = UdpTransport(routing={"*": [("127.0.0.1", 5111)]})
-udp_transport.bind("127.0.0.1", UDP_SERVER_PORT)
-
-cfdp_entity = cfdp.CfdpEntity(
- entity_id=REMOTE_ENTITY_ID, filestore=NativeFileStore("."), transport=udp_transport
-)
-
-input("Running. Press to stop...\n")
-
-cfdp_entity.shutdown()
-udp_transport.unbind()
diff --git a/examples/cfdp-libre-cube-crosstest/requirements.txt b/examples/cfdp-libre-cube-crosstest/requirements.txt
deleted file mode 100644
index 347e79e0..00000000
--- a/examples/cfdp-libre-cube-crosstest/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-cfdp @ git+https://gitlab.com/librecube/lib/python-cfdp.git
diff --git a/examples/cfdp-libre-cube-crosstest/tmtccmd-client.py b/examples/cfdp-libre-cube-crosstest/tmtccmd-client.py
deleted file mode 100755
index 554b78bc..00000000
--- a/examples/cfdp-libre-cube-crosstest/tmtccmd-client.py
+++ /dev/null
@@ -1,323 +0,0 @@
-#!/usr/bin/env python3
-"""This example shows a end-to-end transfer of a small file using the CFDP high level
-components provided by the tmtccmd package."""
-import argparse
-import logging
-import os
-import select
-import socket
-import threading
-import time
-from dataclasses import dataclass
-from datetime import timedelta
-from logging import basicConfig
-from pathlib import Path
-from queue import Empty
-from typing import Any
-
-from common import REMOTE_ENTITY_ID as REMOTE_ENTITY_ID_RAW
-from common import SOURCE_ENTITY_ID as SOURCE_ENTITY_ID_RAW
-from common import UDP_SERVER_PORT, UDP_TM_SERVER_PORT, REMOTE_CFG_FOR_DEST_ENTITY
-from spacepackets.cfdp import (
- ConditionCode,
- TransactionId,
- TransmissionMode,
-)
-from spacepackets.cfdp.pdu.helper import PduFactory
-from spacepackets.util import ByteFieldU16, UnsignedByteField
-
-from tmtccmd.cfdp import CfdpState
-from tmtccmd.cfdp.exceptions import InvalidSourceId
-from tmtccmd.cfdp.handler import SourceHandler
-from tmtccmd.cfdp.mib import (
- CheckTimerProvider,
- DefaultFaultHandlerBase,
- EntityType,
- IndicationCfg,
- LocalEntityCfg,
- RemoteEntityCfgTable,
-)
-from tmtccmd.cfdp.request import PutRequest
-from tmtccmd.cfdp.user import (
- CfdpUserBase,
- FileSegmentRecvdParams,
- MetadataRecvParams,
- TransactionFinishedParams,
-)
-from tmtccmd.util.countdown import Countdown
-from tmtccmd.util.seqcnt import SeqCountProvider
-
-SOURCE_ENTITY_ID = ByteFieldU16(SOURCE_ENTITY_ID_RAW)
-DEST_ENTITY_ID = ByteFieldU16(REMOTE_ENTITY_ID_RAW)
-
-FILE_CONTENT = "Hello World!"
-SOURCE_FILE = Path("files/local.txt")
-DEST_FILE = Path("files/remote.txt")
-
-
-@dataclass
-class TransferParams:
- transmission_mode: TransmissionMode
- verbose_level: int
- no_closure: bool
-
-
-_LOGGER = logging.getLogger()
-
-
-class CfdpFaultHandler(DefaultFaultHandlerBase):
- def notice_of_suspension_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- _LOGGER.warning(
- f"Received Notice of Suspension for transaction {transaction_id!r} with condition "
- f"code {cond!r}. Progress: {progress}"
- )
-
- def notice_of_cancellation_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- _LOGGER.warning(
- f"Received Notice of Cancellation for transaction {transaction_id!r} with condition "
- f"code {cond!r}. Progress: {progress}"
- )
-
- def abandoned_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- _LOGGER.warning(
- f"Received Abanadoned Fault for transaction {transaction_id!r} with condition "
- f"code {cond!r}. Progress: {progress}"
- )
-
- def ignore_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- _LOGGER.warning(
- f"Ignored Fault for transaction {transaction_id!r} with condition "
- f"code {cond!r}. Progress: {progress}"
- )
-
-
-class CfdpUser(CfdpUserBase):
- def __init__(self, base_str: str):
- self.base_str = base_str
- super().__init__()
-
- def transaction_indication(self, transaction_id: TransactionId):
- """This indication is used to report the transaction ID to the CFDP user"""
- _LOGGER.info(f"{self.base_str}: Transaction.indication for {transaction_id}")
-
- def eof_sent_indication(self, transaction_id: TransactionId):
- _LOGGER.info(f"{self.base_str}: EOF-Sent.indication for {transaction_id}")
-
- def transaction_finished_indication(self, params: TransactionFinishedParams):
- _LOGGER.info(
- f"{self.base_str}: Transaction-Finished.indication for {params.transaction_id}."
- )
-
- def metadata_recv_indication(self, params: MetadataRecvParams):
- _LOGGER.info(
- f"{self.base_str}: Metadata-Recv.indication for {params.transaction_id}."
- )
-
- def file_segment_recv_indication(self, params: FileSegmentRecvdParams):
- _LOGGER.info(
- f"{self.base_str}: File-Segment-Recv.indication for {params.transaction_id}."
- )
-
- def report_indication(self, transaction_id: TransactionId, status_report: Any):
- # TODO: p.28 of the CFDP standard specifies what information the status report parameter
- # could contain. I think it would be better to not hardcode the type of the status
- # report here, but something like Union[any, CfdpStatusReport] with CfdpStatusReport
- # being an implementation which supports all three information suggestions would be
- # nice
- pass
-
- def suspended_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode
- ):
- _LOGGER.info(
- f"{self.base_str}: Suspended.indication for {transaction_id} | Condition Code: {cond_code}"
- )
-
- def resumed_indication(self, transaction_id: TransactionId, progress: int):
- _LOGGER.info(
- f"{self.base_str}: Resumed.indication for {transaction_id} | Progress: {progress} bytes"
- )
-
- def fault_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode, progress: int
- ):
- _LOGGER.info(
- f"{self.base_str}: Fault.indication for {transaction_id} | Condition Code: {cond_code} | "
- f"Progress: {progress} bytes"
- )
-
- def abandoned_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode, progress: int
- ):
- _LOGGER.info(
- f"{self.base_str}: Abandoned.indication for {transaction_id} | Condition Code: {cond_code} |"
- f" Progress: {progress} bytes"
- )
-
- def eof_recv_indication(self, transaction_id: TransactionId):
- _LOGGER.info(f"{self.base_str}: EOF-Recv.indication for {transaction_id}")
-
-
-class CustomCheckTimerProvider(CheckTimerProvider):
- def provide_check_timer(
- self,
- local_entity_id: UnsignedByteField,
- remote_entity_id: UnsignedByteField,
- entity_type: EntityType,
- ) -> Countdown:
- return Countdown(timedelta(seconds=5.0))
-
-
-def main():
- help_txt = (
- "This mini application cross tests the tmtccmd CFDP support and the LibreCube CFDP "
- "implementation "
- )
- parser = argparse.ArgumentParser(
- prog="CFDP Libre Cube Cross Testing Application", description=help_txt
- )
- parser.add_argument("-t", "--type", choices=["nak", "ack"], default="ack")
- parser.add_argument("-v", "--verbose", action="count", default=0)
- parser.add_argument("--no-closure", action="store_true", default=False)
- args = parser.parse_args()
- if args.type == "nak":
- transmission_mode = TransmissionMode.UNACKNOWLEDGED
- elif args.type == "ack":
- transmission_mode = TransmissionMode.ACKNOWLEDGED
- else:
- transmission_mode = None
- if args.verbose == 0:
- logging_level = logging.INFO
- elif args.verbose >= 1:
- logging_level = logging.DEBUG
- transfer_params = TransferParams(transmission_mode, args.verbose, args.no_closure)
- basicConfig(level=logging_level)
-
- # If the test files already exist, delete them.
- if SOURCE_FILE.exists():
- os.remove(SOURCE_FILE)
- with open(SOURCE_FILE, "w") as file:
- file.write(FILE_CONTENT)
-
- remote_cfg_table = RemoteEntityCfgTable([REMOTE_CFG_FOR_DEST_ENTITY])
-
- # Enable all indications.
- src_indication_cfg = IndicationCfg()
- src_fault_handler = CfdpFaultHandler()
- src_entity_cfg = LocalEntityCfg(
- SOURCE_ENTITY_ID, src_indication_cfg, src_fault_handler
- )
- # 16 bit sequence count for transactions.
- src_seq_count_provider = SeqCountProvider(16)
- src_user = CfdpUser("SRC ENTITY")
- check_timer_provider = CustomCheckTimerProvider()
- source_handler = SourceHandler(
- cfg=src_entity_cfg,
- seq_num_provider=src_seq_count_provider,
- remote_cfg_table=remote_cfg_table,
- user=src_user,
- check_timer_provider=check_timer_provider,
- )
-
- udp_client = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
- udp_tm_server = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
- udp_tm_server.bind(("127.0.0.1", UDP_TM_SERVER_PORT))
- udp_tm_server.setblocking(False)
-
- # Spawn a new thread and move the source handler there. This is scalable: If multiple number
- # of concurrent file operations are required, a new thread with a new source handler can
- # be spawned for each one.
- source_thread = threading.Thread(
- target=source_entity_handler,
- args=[udp_client, udp_tm_server, transfer_params, source_handler],
- )
-
- source_thread.start()
- source_thread.join()
-
- src_file_content = None
- with open(SOURCE_FILE) as file:
- src_file_content = file.read()
- dest_file_content = None
- if not DEST_FILE.exists():
- raise ValueError("Destination file does not exist!")
- with open(DEST_FILE) as file:
- dest_file_content = file.read()
- assert src_file_content == dest_file_content
- _LOGGER.info("Source and destination file content are equal. Deleting files.")
- if SOURCE_FILE.exists():
- os.remove(SOURCE_FILE)
- if DEST_FILE.exists():
- os.remove(DEST_FILE)
- _LOGGER.info("Done.")
-
-
-def source_entity_handler(
- tc_client: socket.socket,
- tm_client: socket.socket,
- transfer_params: TransferParams,
- source_handler: SourceHandler,
-):
- # This put request could in principle also be sent from something like a front end application.
- put_request = PutRequest(
- destination_id=DEST_ENTITY_ID,
- source_file=SOURCE_FILE,
- dest_file=DEST_FILE,
- trans_mode=transfer_params.transmission_mode,
- closure_requested=not transfer_params.no_closure,
- )
- no_packet_received = False
- print(f"SRC HANDLER: Inserting Put Request: {put_request}")
- with open(SOURCE_FILE) as file:
- file_content = file.read()
- print(f"File content of source file {SOURCE_FILE}: {file_content}")
- source_handler.put_request(put_request)
- while True:
- try:
- ready = select.select([tm_client], [], [], 0)
- if ready[0]:
- data, _ = tm_client.recvfrom(4096)
- packet = PduFactory.from_raw(data)
- try:
- source_handler.insert_packet(packet)
- except InvalidSourceId:
- _LOGGER.warning(f"invalid source ID in packet {packet}")
- no_packet_received = False
- else:
- no_packet_received = True
- except Empty:
- no_packet_received = True
- fsm_result = source_handler.state_machine()
- no_packet_sent = False
- if fsm_result.states.num_packets_ready > 0:
- while fsm_result.states.num_packets_ready > 0:
- next_pdu_wrapper = source_handler.get_next_packet()
- assert next_pdu_wrapper is not None
- if transfer_params.verbose_level >= 1:
- _LOGGER.debug(f"SRC Handler: Sending packet {next_pdu_wrapper.pdu}")
- # Send all packets which need to be sent.
- tc_client.sendto(
- next_pdu_wrapper.pdu.pack(), ("127.0.0.1", UDP_SERVER_PORT)
- )
- no_packet_sent = False
- else:
- no_packet_sent = True
- # If there is no work to do, put the thread to sleep.
- if no_packet_received and no_packet_sent:
- time.sleep(0.5)
- # Transaction done
- if fsm_result.states.state == CfdpState.IDLE:
- _LOGGER.info("Source entity operation done.")
- break
-
-
-if __name__ == "__main__":
- main()
diff --git a/examples/cfdp-simple/README.md b/examples/cfdp-simple/README.md
deleted file mode 100644
index 621f97de..00000000
--- a/examples/cfdp-simple/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-CFDP Simple File Copy Example
-===========
-
-This example shows an end-to-end file transfer on a host computer. This should give you a general
-idea of how the source and destination handler work in practice. Simply running the script with
-
-
-```sh
-./file-copy-example.py
-```
-
-will perform an acknowledged transfer of a file on the same host system.
-You can also perform an unacknowledged transfer using
-
-```sh
-./file-copy-example.py -t nak
-```
-
-It is also possible to supply the ``-v`` verbosity argument to the application to print all
-packets being exchanged between the source and destination handler.
diff --git a/examples/cfdp-simple/file-copy-example.py b/examples/cfdp-simple/file-copy-example.py
deleted file mode 100755
index 347c3c8d..00000000
--- a/examples/cfdp-simple/file-copy-example.py
+++ /dev/null
@@ -1,383 +0,0 @@
-#!/usr/bin/env python3
-"""This example shows a end-to-end transfer of a small file using the CFDP high level
-components provided by the tmtccmd package."""
-import argparse
-import copy
-import logging
-import os
-import threading
-import time
-from dataclasses import dataclass
-from datetime import timedelta
-from logging import basicConfig
-from multiprocessing import Queue
-from pathlib import Path
-from queue import Empty
-from typing import Any
-
-from spacepackets.cfdp import GenericPduPacket, TransactionId
-from spacepackets.cfdp.defs import ChecksumType, ConditionCode, TransmissionMode
-from spacepackets.cfdp.pdu import AbstractFileDirectiveBase
-from spacepackets.util import ByteFieldU16, UnsignedByteField
-
-from tmtccmd.cfdp import CfdpState
-from tmtccmd.cfdp.handler.dest import DestHandler
-from tmtccmd.cfdp.handler.source import SourceHandler
-from tmtccmd.cfdp.mib import (
- CheckTimerProvider,
- DefaultFaultHandlerBase,
- EntityType,
- IndicationCfg,
- LocalEntityCfg,
- RemoteEntityCfg,
- RemoteEntityCfgTable,
-)
-from tmtccmd.cfdp.request import PutRequest
-from tmtccmd.cfdp.user import (
- CfdpUserBase,
- FileSegmentRecvdParams,
- MetadataRecvParams,
- TransactionFinishedParams,
-)
-from tmtccmd.util.countdown import Countdown
-from tmtccmd.util.seqcnt import SeqCountProvider
-
-SOURCE_ENTITY_ID = ByteFieldU16(1)
-DEST_ENTITY_ID = ByteFieldU16(2)
-
-FILE_CONTENT = "Hello World!\n"
-FILE_SEGMENT_SIZE = len(FILE_CONTENT)
-MAX_PACKET_LEN = 512
-SOURCE_FILE = Path("/tmp/cfdp-test-source.txt")
-DEST_FILE = Path("/tmp/cfdp-test-dest.txt")
-
-
-@dataclass
-class TransferParams:
- transmission_mode: TransmissionMode
- verbose_level: int
- no_closure: bool
-
-
-_LOGGER = logging.getLogger()
-
-
-REMOTE_CFG_FOR_SOURCE_ENTITY = RemoteEntityCfg(
- entity_id=SOURCE_ENTITY_ID,
- max_packet_len=MAX_PACKET_LEN,
- max_file_segment_len=FILE_SEGMENT_SIZE,
- closure_requested=True,
- crc_on_transmission=False,
- default_transmission_mode=TransmissionMode.ACKNOWLEDGED,
- crc_type=ChecksumType.CRC_32,
-)
-REMOTE_CFG_FOR_DEST_ENTITY = copy.copy(REMOTE_CFG_FOR_SOURCE_ENTITY)
-REMOTE_CFG_FOR_DEST_ENTITY.entity_id = DEST_ENTITY_ID
-
-# These queues will be used to exchange PDUs between threads.
-SOURCE_TO_DEST_QUEUE = Queue()
-DEST_TO_SOURCE_QUEUE = Queue()
-
-
-class CfdpFaultHandler(DefaultFaultHandlerBase):
- def notice_of_suspension_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- _LOGGER.warning(
- f"Received Notice of Suspension for transaction {transaction_id!r} with condition "
- f"code {cond!r}. Progress: {progress}"
- )
-
- def notice_of_cancellation_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- _LOGGER.warning(
- f"Received Notice of Cancellation for transaction {transaction_id!r} with condition "
- f"code {cond!r}. Progress: {progress}"
- )
-
- def abandoned_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- _LOGGER.warning(
- f"Received Abanadoned Fault for transaction {transaction_id!r} with condition "
- f"code {cond!r}. Progress: {progress}"
- )
-
- def ignore_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- _LOGGER.warning(
- f"Ignored Fault for transaction {transaction_id!r} with condition "
- f"code {cond!r}. Progress: {progress}"
- )
-
-
-class CfdpUser(CfdpUserBase):
- def __init__(self, base_str: str):
- self.base_str = base_str
- super().__init__()
-
- def transaction_indication(self, transaction_id: TransactionId):
- """This indication is used to report the transaction ID to the CFDP user"""
- _LOGGER.info(f"{self.base_str}: Transaction.indication for {transaction_id}")
-
- def eof_sent_indication(self, transaction_id: TransactionId):
- _LOGGER.info(f"{self.base_str}: EOF-Sent.indication for {transaction_id}")
-
- def transaction_finished_indication(self, params: TransactionFinishedParams):
- _LOGGER.info(
- f"{self.base_str}: Transaction-Finished.indication for {params.transaction_id}."
- )
-
- def metadata_recv_indication(self, params: MetadataRecvParams):
- _LOGGER.info(
- f"{self.base_str}: Metadata-Recv.indication for {params.transaction_id}."
- )
-
- def file_segment_recv_indication(self, params: FileSegmentRecvdParams):
- _LOGGER.info(
- f"{self.base_str}: File-Segment-Recv.indication for {params.transaction_id}."
- )
-
- def report_indication(self, transaction_id: TransactionId, status_report: Any):
- # TODO: p.28 of the CFDP standard specifies what information the status report parameter
- # could contain. I think it would be better to not hardcode the type of the status
- # report here, but something like Union[any, CfdpStatusReport] with CfdpStatusReport
- # being an implementation which supports all three information suggestions would be
- # nice
- pass
-
- def suspended_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode
- ):
- _LOGGER.info(
- f"{self.base_str}: Suspended.indication for {transaction_id} | Condition Code: {cond_code}"
- )
-
- def resumed_indication(self, transaction_id: TransactionId, progress: int):
- _LOGGER.info(
- f"{self.base_str}: Resumed.indication for {transaction_id} | Progress: {progress} bytes"
- )
-
- def fault_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode, progress: int
- ):
- _LOGGER.info(
- f"{self.base_str}: Fault.indication for {transaction_id} | Condition Code: {cond_code} | "
- f"Progress: {progress} bytes"
- )
-
- def abandoned_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode, progress: int
- ):
- _LOGGER.info(
- f"{self.base_str}: Abandoned.indication for {transaction_id} | Condition Code: {cond_code} |"
- f" Progress: {progress} bytes"
- )
-
- def eof_recv_indication(self, transaction_id: TransactionId):
- _LOGGER.info(f"{self.base_str}: EOF-Recv.indication for {transaction_id}")
-
-
-class CustomCheckTimerProvider(CheckTimerProvider):
- def provide_check_timer(
- self,
- local_entity_id: UnsignedByteField,
- remote_entity_id: UnsignedByteField,
- entity_type: EntityType,
- ) -> Countdown:
- return Countdown(timedelta(seconds=5.0))
-
-
-def main():
- help_txt = (
- "This mini application shows the source and destination entity handlers in action. "
- "You can configure the transmission mode with the -t argument, which defaults to the "
- "acknowledged mode. It is also possible to increase the verbosity level to print all "
- "packets being exchanged."
- )
- parser = argparse.ArgumentParser(
- prog="CFDP File Copy Example Application", description=help_txt
- )
- parser.add_argument("-t", "--type", choices=["nak", "ack"], default="ack")
- parser.add_argument("-v", "--verbose", action="count", default=0)
- parser.add_argument("--no-closure", action="store_true", default=False)
- args = parser.parse_args()
- if args.type == "nak":
- transmission_mode = TransmissionMode.UNACKNOWLEDGED
- elif args.type == "ack":
- transmission_mode = TransmissionMode.ACKNOWLEDGED
- else:
- transmission_mode = None
- if args.verbose == 0:
- logging_level = logging.INFO
- elif args.verbose >= 1:
- logging_level = logging.DEBUG
- transfer_params = TransferParams(transmission_mode, args.verbose, args.no_closure)
- basicConfig(level=logging_level)
-
- # If the test files already exist, delete them.
- if SOURCE_FILE.exists():
- os.remove(SOURCE_FILE)
- if DEST_FILE.exists():
- os.remove(DEST_FILE)
- with open(SOURCE_FILE, "w") as file:
- file.write(FILE_CONTENT)
-
- remote_cfg_table = RemoteEntityCfgTable()
- remote_cfg_table.add_config(REMOTE_CFG_FOR_SOURCE_ENTITY)
- remote_cfg_table.add_config(REMOTE_CFG_FOR_DEST_ENTITY)
-
- # Enable all indications.
- src_indication_cfg = IndicationCfg()
- src_fault_handler = CfdpFaultHandler()
- src_entity_cfg = LocalEntityCfg(
- SOURCE_ENTITY_ID, src_indication_cfg, src_fault_handler
- )
- # 16 bit sequence count for transactions.
- src_seq_count_provider = SeqCountProvider(16)
- src_user = CfdpUser("SRC ENTITY")
- check_timer_provider = CustomCheckTimerProvider()
- source_handler = SourceHandler(
- cfg=src_entity_cfg,
- seq_num_provider=src_seq_count_provider,
- remote_cfg_table=remote_cfg_table,
- user=src_user,
- check_timer_provider=check_timer_provider,
- )
- # Spawn a new thread and move the source handler there. This is scalable: If multiple number
- # of concurrent file operations are required, a new thread with a new source handler can
- # be spawned for each one.
- source_thread = threading.Thread(
- target=source_entity_handler, args=[transfer_params, source_handler]
- )
-
- # Enable all indications.
- dest_indication_cfg = IndicationCfg()
- dest_fault_handler = CfdpFaultHandler()
- dest_entity_cfg = LocalEntityCfg(
- DEST_ENTITY_ID, dest_indication_cfg, dest_fault_handler
- )
- dest_user = CfdpUser("DEST ENTITY")
- dest_handler = DestHandler(
- cfg=dest_entity_cfg,
- user=dest_user,
- remote_cfg_table=remote_cfg_table,
- check_timer_provider=check_timer_provider,
- )
- # Spawn a new thread and move the destination handler there. This is scalable. One example
- # approach could be to keep a dictionary of active file copy operations, where the transaction
- # ID is the key. If a new Metadata PDU with a new transaction ID is detected, a new
- # destination handler in a new thread could be spawned to handle the file copy operation.
- dest_thread = threading.Thread(
- target=dest_entity_handler, args=[transfer_params, dest_handler]
- )
-
- source_thread.start()
- dest_thread.start()
- source_thread.join()
- dest_thread.join()
-
- src_file_content = None
- with open(SOURCE_FILE) as file:
- src_file_content = file.read()
- dest_file_content = None
- with open(DEST_FILE) as file:
- dest_file_content = file.read()
- assert src_file_content == dest_file_content
- _LOGGER.info("Source and destination file content are equal. Deleting files.")
- if SOURCE_FILE.exists():
- os.remove(SOURCE_FILE)
- if DEST_FILE.exists():
- os.remove(DEST_FILE)
- _LOGGER.info("Done.")
-
-
-def source_entity_handler(
- transfer_params: TransferParams, source_handler: SourceHandler
-):
- # This put request could in principle also be sent from something like a front end application.
- put_request = PutRequest(
- destination_id=DEST_ENTITY_ID,
- source_file=SOURCE_FILE,
- dest_file=DEST_FILE,
- trans_mode=transfer_params.transmission_mode,
- closure_requested=not transfer_params.no_closure,
- )
- no_packet_received = False
- print(f"SRC HANDLER: Inserting Put Request: {put_request}")
- with open(SOURCE_FILE) as file:
- file_content = file.read()
- print(f"File content of source file {SOURCE_FILE}: {file_content}")
- assert source_handler.put_request(put_request)
- while True:
- try:
- # We are getting the packets from a Queue here, they could for example also be polled
- # from a network.
- packet: AbstractFileDirectiveBase = DEST_TO_SOURCE_QUEUE.get(False)
- source_handler.insert_packet(packet)
- no_packet_received = False
- except Empty:
- no_packet_received = True
- fsm_result = source_handler.state_machine()
- no_packet_sent = False
- if fsm_result.states.num_packets_ready > 0:
- while fsm_result.states.num_packets_ready > 0:
- next_pdu_wrapper = source_handler.get_next_packet()
- assert next_pdu_wrapper is not None
- if transfer_params.verbose_level >= 1:
- _LOGGER.debug(f"SRC Handler: Sending packet {next_pdu_wrapper.pdu}")
- # Send all packets which need to be sent.
- SOURCE_TO_DEST_QUEUE.put(next_pdu_wrapper.pdu)
- no_packet_sent = False
- else:
- no_packet_sent = True
- # If there is no work to do, put the thread to sleep.
- if no_packet_received and no_packet_sent:
- time.sleep(0.5)
- # Transaction done
- if fsm_result.states.state == CfdpState.IDLE:
- _LOGGER.info("Source entity operation done.")
- break
-
-
-def dest_entity_handler(transfer_params: TransferParams, dest_handler: DestHandler):
- first_packet = True
- no_packet_received = False
- while True:
- try:
- packet: GenericPduPacket = SOURCE_TO_DEST_QUEUE.get(False)
- dest_handler.insert_packet(packet)
- no_packet_received = False
- if first_packet:
- first_packet = False
- except Empty:
- no_packet_received = True
- fsm_result = dest_handler.state_machine()
- if fsm_result.states.num_packets_ready > 0:
- no_packet_sent = False
- while fsm_result.states.num_packets_ready > 0:
- next_pdu_wrapper = dest_handler.get_next_packet()
- assert next_pdu_wrapper is not None
- if transfer_params.verbose_level >= 1:
- _LOGGER.debug(
- f"DEST Handler: Sending packet {next_pdu_wrapper.pdu}"
- )
- DEST_TO_SOURCE_QUEUE.put(next_pdu_wrapper.pdu)
- else:
- no_packet_sent = True
- # If there is no work to do, put the thread to sleep.
- if no_packet_received and no_packet_sent:
- time.sleep(0.5)
- # Transaction done
- if not first_packet and fsm_result.states.state == CfdpState.IDLE:
- _LOGGER.info("Destination entity operation done.")
- break
- with open(DEST_FILE) as file:
- file_content = file.read()
- print(f"File content of destination file {DEST_FILE}: {file_content}")
-
-
-if __name__ == "__main__":
- main()
diff --git a/pyproject.toml b/pyproject.toml
index 78cb6e8b..a5880f52 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,14 +36,14 @@ dependencies = [
"Deprecated~=1.2",
"pyserial~=3.5",
"dle-encoder~=0.2.3",
- "spacepackets~=0.21.0",
+ "spacepackets~=0.23.0",
+ "cfdp-py~=0.1.0"
# "spacepackets @ git+https://github.com/us-irs/spacepackets-py@main"
]
[project.optional-dependencies]
gui = [
"PyQt6~=6.6",
- # "PyQt5-stubs~=5.15",
]
test = [
"pyfakefs~=4.5",
diff --git a/tests/cfdp/__init__.py b/tests/cfdp/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/cfdp/cfdp_fault_handler_mock.py b/tests/cfdp/cfdp_fault_handler_mock.py
deleted file mode 100644
index 8d636332..00000000
--- a/tests/cfdp/cfdp_fault_handler_mock.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from spacepackets.cfdp import ConditionCode
-from tmtccmd.cfdp.mib import DefaultFaultHandlerBase
-from tmtccmd.cfdp import TransactionId
-
-
-class FaultHandler(DefaultFaultHandlerBase):
- def __init__(self):
- super().__init__()
-
- def notice_of_suspension_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- pass
-
- def notice_of_cancellation_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- pass
-
- def abandoned_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- pass
-
- def ignore_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- pass
diff --git a/tests/cfdp/cfdp_user_mock.py b/tests/cfdp/cfdp_user_mock.py
deleted file mode 100644
index 9f92909c..00000000
--- a/tests/cfdp/cfdp_user_mock.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from spacepackets.cfdp import ConditionCode
-from tmtccmd.cfdp import CfdpUserBase, TransactionId
-from tmtccmd.cfdp.user import (
- FileSegmentRecvdParams,
- MetadataRecvParams,
- TransactionFinishedParams,
- TransactionParams,
-)
-
-
-class CfdpUser(CfdpUserBase):
- def __init__(self):
- super().__init__()
-
- def transaction_indication(self, transaction_params: TransactionParams):
- pass
-
- def transaction_finished_indication(self, params: TransactionFinishedParams):
- pass
-
- def eof_sent_indication(self, transaction_id: TransactionId):
- pass
-
- def abandon_indication(
- self, transaction_id: int, cond_code: ConditionCode, progress: int
- ):
- pass
-
- def metadata_recv_indication(self, params: MetadataRecvParams):
- pass
-
- def file_segment_recv_indication(self, params: FileSegmentRecvdParams):
- pass
-
- def report_indication(self, transaction_id: TransactionId, status_report: any):
- pass
-
- def suspended_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode
- ):
- pass
-
- def resumed_indication(self, transaction_id: TransactionId, progress: int):
- pass
-
- def fault_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode, progress: int
- ):
- pass
-
- def abandoned_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode, progress: int
- ):
- pass
-
- def eof_recv_indication(self, transaction_id: TransactionId):
- pass
diff --git a/tests/cfdp/common.py b/tests/cfdp/common.py
deleted file mode 100644
index ad2679db..00000000
--- a/tests/cfdp/common.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from datetime import timedelta
-from spacepackets.util import UnsignedByteField
-from tmtccmd.cfdp.mib import CheckTimerProvider, EntityType
-from tmtccmd.util.countdown import Countdown
-
-
-class CheckTimerProviderForTest(CheckTimerProvider):
- def __init__(
- self, timeout_dest_entity_ms: int = 50, timeout_source_entity_ms: int = 50
- ) -> None:
- self.timeout_dest_entity_ms = timeout_dest_entity_ms
- self.timeout_src_entity_ms = timeout_source_entity_ms
-
- def provide_check_timer(
- self,
- local_entity_id: UnsignedByteField,
- remote_entity_id: UnsignedByteField,
- entity_type: EntityType,
- ) -> Countdown:
- if entity_type == EntityType.RECEIVING:
- return Countdown(timedelta(milliseconds=self.timeout_dest_entity_ms))
- else:
- return Countdown(timedelta(milliseconds=self.timeout_src_entity_ms))
diff --git a/tests/cfdp/test_checksum.py b/tests/cfdp/test_checksum.py
deleted file mode 100644
index 06bddc30..00000000
--- a/tests/cfdp/test_checksum.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import os
-import struct
-from pyfakefs.fake_filesystem_unittest import TestCase
-from tempfile import gettempdir
-from pathlib import Path
-from tmtccmd.cfdp.handler.crc import CrcHelper, calc_modular_checksum
-from tmtccmd.cfdp.user import HostFilestore
-from spacepackets.cfdp import ChecksumType
-
-
-EXAMPLE_DATA_CFDP = bytes(
- [
- 0x00,
- 0x01,
- 0x02,
- 0x03,
- 0x04,
- 0x05,
- 0x06,
- 0x07,
- 0x08,
- 0x09,
- 0x0A,
- 0x0B,
- 0x0C,
- 0x0D,
- 0x0E,
- ]
-)
-
-
-class TestChecksumHelper(TestCase):
- def setUp(self):
- self.setUpPyfakefs()
- self.crc_helper = CrcHelper(ChecksumType.NULL_CHECKSUM, HostFilestore())
- self.file_path = Path(f"{gettempdir()}/crc_file")
- with open(self.file_path, "wb") as file:
- file.write(EXAMPLE_DATA_CFDP)
- segments_to_add = []
- for i in range(4):
- if (i + 1) * 4 > len(EXAMPLE_DATA_CFDP):
- data_to_add = EXAMPLE_DATA_CFDP[i * 4 :].ljust(4, bytes([0]))
- else:
- data_to_add = EXAMPLE_DATA_CFDP[i * 4 : (i + 1) * 4]
- segments_to_add.append(
- int.from_bytes(
- data_to_add,
- byteorder="big",
- signed=False,
- )
- )
- full_sum = sum(segments_to_add)
- print(full_sum)
- full_sum %= 2**32
-
- self.expected_checksum_for_example = struct.pack("!I", full_sum)
-
- def test_modular_checksum(self):
- self.assertEqual(
- calc_modular_checksum(self.file_path), self.expected_checksum_for_example
- )
-
- def tearDown(self):
- if self.file_path.exists():
- os.remove(self.file_path)
diff --git a/tests/cfdp/test_dest_handler.py b/tests/cfdp/test_dest_handler.py
deleted file mode 100644
index a211f4cb..00000000
--- a/tests/cfdp/test_dest_handler.py
+++ /dev/null
@@ -1,359 +0,0 @@
-import dataclasses
-import os
-import tempfile
-from pyfakefs.fake_filesystem_unittest import TestCase
-from pathlib import Path
-from typing import Optional, cast, List
-from unittest.mock import MagicMock
-
-from spacepackets.cfdp import (
- ChecksumType,
- ConditionCode,
- Direction,
- DirectiveType,
- FinishedParams,
- PduConfig,
- PduType,
- TransactionId,
- TransmissionMode,
- NULL_CHECKSUM_U32,
-)
-from spacepackets.cfdp.tlv import (
- MessageToUserTlv,
- TlvList,
- OriginatingTransactionId,
- ProxyPutResponse,
- ProxyPutResponseParams,
-)
-from spacepackets.cfdp.pdu import (
- DeliveryCode,
- EofPdu,
- FileDataPdu,
- FileStatus,
- FinishedPdu,
- MetadataPdu,
-)
-from spacepackets.cfdp.pdu.file_data import FileDataParams
-from spacepackets.cfdp.pdu.metadata import MetadataParams
-from spacepackets.util import ByteFieldU8, ByteFieldU16
-
-from tmtccmd.cfdp import (
- IndicationCfg,
- LocalEntityCfg,
- RemoteEntityCfg,
- RemoteEntityCfgTable,
-)
-from tmtccmd.cfdp.defs import CfdpState
-from tmtccmd.cfdp.handler.dest import (
- DestHandler,
- FsmResult,
- TransactionStep,
-)
-from tmtccmd.cfdp.user import (
- FileSegmentRecvdParams,
- TransactionFinishedParams,
- MetadataRecvParams,
-)
-
-from .cfdp_fault_handler_mock import FaultHandler
-from .cfdp_user_mock import CfdpUser
-from .common import CheckTimerProviderForTest
-
-
-@dataclasses.dataclass
-class FileInfo:
- rand_data: bytes
- file_size: int
- crc32: bytes
-
-
-class TestDestHandlerBase(TestCase):
- def common_setup(self, trans_mode: TransmissionMode):
- self.setUpPyfakefs()
- self.indication_cfg = IndicationCfg(True, True, True, True, True, True)
- self.fault_handler = FaultHandler()
- self.fault_handler.notice_of_cancellation_cb = MagicMock()
- self.entity_id = ByteFieldU16(2)
- self.local_cfg = LocalEntityCfg(
- self.entity_id, self.indication_cfg, self.fault_handler
- )
- self.src_entity_id = ByteFieldU16(1)
- self.src_pdu_conf = PduConfig(
- source_entity_id=self.src_entity_id,
- dest_entity_id=self.entity_id,
- transaction_seq_num=ByteFieldU8(1),
- trans_mode=trans_mode,
- )
- self.transaction_id = TransactionId(self.src_entity_id, ByteFieldU8(1))
- self.expected_mode = trans_mode
- self.closure_requested = False
- self.cfdp_user = CfdpUser()
- self.cfdp_user.transaction_indication = MagicMock()
- self.cfdp_user.eof_recv_indication = MagicMock()
- self.cfdp_user.file_segment_recv_indication = MagicMock()
- self.cfdp_user.metadata_recv_indication = MagicMock()
- self.cfdp_user.transaction_finished_indication = MagicMock()
- self.file_segment_len = 128
- self.remote_cfg_table = RemoteEntityCfgTable()
- self.timeout_nak_procedure_seconds = 0.05
- self.timeout_positive_ack_procedure_seconds = 0.05
- self.remote_cfg = RemoteEntityCfg(
- entity_id=self.src_entity_id,
- check_limit=2,
- crc_type=ChecksumType.CRC_32,
- closure_requested=False,
- crc_on_transmission=False,
- default_transmission_mode=TransmissionMode.UNACKNOWLEDGED,
- max_file_segment_len=self.file_segment_len,
- max_packet_len=self.file_segment_len,
- nak_timer_expiration_limit=2,
- nak_timer_interval_seconds=self.timeout_nak_procedure_seconds,
- positive_ack_timer_interval_seconds=self.timeout_positive_ack_procedure_seconds,
- positive_ack_timer_expiration_limit=2,
- )
- self.remote_cfg_table.add_config(self.remote_cfg)
- self.timeout_check_limit_handling_ms = 50
- self.dest_handler = DestHandler(
- self.local_cfg,
- self.cfdp_user,
- self.remote_cfg_table,
- CheckTimerProviderForTest(
- timeout_dest_entity_ms=self.timeout_check_limit_handling_ms
- ),
- )
- self.src_file_path, self.dest_file_path = Path(
- f"{tempfile.gettempdir()}/source"
- ), Path(f"{tempfile.gettempdir()}/dest")
-
- def _state_checker(
- self,
- fsm_res: Optional[FsmResult],
- num_packets_ready: int,
- expected_state: CfdpState,
- expected_transaction: TransactionStep,
- ):
- if fsm_res is not None:
- self.assertEqual(fsm_res.states.state, expected_state)
- self.assertEqual(fsm_res.states.step, expected_transaction)
- self.assertEqual(fsm_res.states.num_packets_ready, num_packets_ready)
- if num_packets_ready > 0:
- self.assertTrue(fsm_res.states.packets_ready)
- if expected_state != CfdpState.IDLE:
- self.assertEqual(self.dest_handler.transmission_mode, self.expected_mode)
- self.assertEqual(self.dest_handler.states.state, expected_state)
- self.assertEqual(self.dest_handler.states.step, expected_transaction)
- self.assertEqual(self.dest_handler.state, expected_state)
- self.assertEqual(self.dest_handler.step, expected_transaction)
- self.assertEqual(self.dest_handler.num_packets_ready, num_packets_ready)
-
- def _generic_regular_transfer_init(
- self,
- file_size: int,
- expected_msgs_to_user: Optional[List[MessageToUserTlv]] = None,
- ):
- fsm_res = self._generic_transfer_init(
- file_size, 0, CfdpState.IDLE, TransactionStep.IDLE
- )
- self._state_checker(
- fsm_res, 0, CfdpState.BUSY, TransactionStep.RECEIVING_FILE_DATA
- )
- self.cfdp_user.metadata_recv_indication.assert_called_once()
- self.cfdp_user.metadata_recv_indication.assert_called_with(
- MetadataRecvParams(
- self.transaction_id,
- self.src_pdu_conf.source_entity_id,
- file_size,
- self.src_file_path.as_posix(),
- self.dest_file_path.as_posix(),
- expected_msgs_to_user,
- ),
- )
-
- def _generic_transfer_init(
- self,
- file_size: int,
- expected_init_packets: int,
- expected_init_state: CfdpState,
- expected_init_step: TransactionStep,
- expected_originating_id: Optional[TransactionId] = None,
- ) -> FsmResult:
- checksum_type = ChecksumType.NULL_CHECKSUM
- if file_size > 0:
- checksum_type = ChecksumType.CRC_32
- metadata_params = MetadataParams(
- checksum_type=checksum_type,
- closure_requested=self.closure_requested,
- source_file_name=self.src_file_path.as_posix(),
- dest_file_name=self.dest_file_path.as_posix(),
- file_size=file_size,
- )
- file_transfer_init = MetadataPdu(
- params=metadata_params, pdu_conf=self.src_pdu_conf
- )
- self._state_checker(
- None, expected_init_packets, expected_init_state, expected_init_step
- )
- self.dest_handler.insert_packet(file_transfer_init)
- fsm_res = self.dest_handler.state_machine()
- return fsm_res
-
- def _insert_file_segment(
- self,
- segment: bytes,
- offset: int,
- expected_packets: int = 0,
- expected_step: TransactionStep = TransactionStep.RECEIVING_FILE_DATA,
- check_indication: bool = True,
- ) -> FsmResult:
- fd_params = FileDataParams(file_data=segment, offset=offset)
- file_data_pdu = FileDataPdu(params=fd_params, pdu_conf=self.src_pdu_conf)
- self.dest_handler.insert_packet(file_data_pdu)
- fsm_res = self.dest_handler.state_machine()
- if (
- self.indication_cfg.file_segment_recvd_indication_required
- and check_indication
- ):
- self.cfdp_user.file_segment_recv_indication.assert_called_once()
- self.assertEqual(self.cfdp_user.file_segment_recv_indication.call_count, 1)
- seg_recv_params = cast(
- FileSegmentRecvdParams,
- self.cfdp_user.file_segment_recv_indication.call_args.args[0],
- )
- self.assertEqual(seg_recv_params.transaction_id, self.transaction_id)
- self.cfdp_user.file_segment_recv_indication.reset_mock()
- self._state_checker(
- fsm_res,
- expected_packets,
- CfdpState.BUSY,
- expected_step,
- )
- return fsm_res
-
- def _generic_insert_eof_pdu(self, file_size: int, checksum: bytes) -> FsmResult:
- eof_pdu = EofPdu(
- file_size=file_size, file_checksum=checksum, pdu_conf=self.src_pdu_conf
- )
- self.dest_handler.insert_packet(eof_pdu)
- fsm_res = self.dest_handler.state_machine()
- if self.expected_mode == TransmissionMode.UNACKNOWLEDGED:
- if self.closure_requested:
- self._state_checker(
- fsm_res,
- 1,
- CfdpState.BUSY,
- TransactionStep.SENDING_FINISHED_PDU,
- )
- else:
- self._state_checker(fsm_res, 0, CfdpState.IDLE, TransactionStep.IDLE)
- return fsm_res
-
- def _generic_eof_recv_indication_check(self, fsm_res: FsmResult):
- self.cfdp_user.eof_recv_indication.assert_called_once()
- self.assertEqual(
- self.cfdp_user.eof_recv_indication.call_args.args[0], self.transaction_id
- )
- self.assertEqual(fsm_res.states.transaction_id, self.transaction_id)
-
- def _generic_no_error_finished_pdu_check(
- self,
- fsm_res: FsmResult,
- expected_step: TransactionStep = TransactionStep.SENDING_FINISHED_PDU,
- expected_file_status: FileStatus = FileStatus.FILE_RETAINED,
- expected_condition_code: ConditionCode = ConditionCode.NO_ERROR,
- ) -> FinishedPdu:
- self._state_checker(fsm_res, 1, CfdpState.BUSY, expected_step)
- self.assertTrue(fsm_res.states.packets_ready)
- next_pdu = self.dest_handler.get_next_packet()
- assert next_pdu is not None
- self.assertEqual(next_pdu.pdu_type, PduType.FILE_DIRECTIVE)
- self.assertEqual(next_pdu.pdu_directive_type, DirectiveType.FINISHED_PDU)
-
- finished_pdu = next_pdu.to_finished_pdu()
- self.assertEqual(finished_pdu.condition_code, expected_condition_code)
- self.assertEqual(finished_pdu.file_status, expected_file_status)
- if expected_condition_code == ConditionCode.NO_ERROR:
- self.assertEqual(finished_pdu.delivery_code, DeliveryCode.DATA_COMPLETE)
- else:
- self.assertEqual(finished_pdu.delivery_code, DeliveryCode.DATA_INCOMPLETE)
- self.assertEqual(finished_pdu.direction, Direction.TOWARDS_SENDER)
- self.assertIsNone(finished_pdu.fault_location)
- self.assertEqual(len(finished_pdu.file_store_responses), 0)
- return finished_pdu
-
- def _generic_verify_transfer_completion(
- self,
- fsm_res: FsmResult,
- expected_file_data: Optional[bytes],
- expected_finished_params: FinishedParams = FinishedParams(
- condition_code=ConditionCode.NO_ERROR,
- file_status=FileStatus.FILE_RETAINED,
- delivery_code=DeliveryCode.DATA_COMPLETE,
- ),
- ):
- self._generic_transfer_finished_indication_check(
- fsm_res, expected_finished_params
- )
- if expected_file_data is not None:
- self.assertTrue(self.dest_file_path.exists())
- self.assertEqual(
- self.dest_file_path.stat().st_size, len(expected_file_data)
- )
- with open(self.dest_file_path, "rb") as file:
- self.assertEqual(expected_file_data, file.read())
- fsm_res = self.dest_handler.state_machine()
- if self.expected_mode == TransmissionMode.UNACKNOWLEDGED:
- self._state_checker(fsm_res, 0, CfdpState.IDLE, TransactionStep.IDLE)
- else:
- self._state_checker(
- fsm_res, 0, CfdpState.BUSY, TransactionStep.WAITING_FOR_FINISHED_ACK
- )
-
- def _generic_transfer_finished_indication_check(
- self, fsm_res: FsmResult, expected_finished_params: FinishedParams
- ):
- self.cfdp_user.transaction_finished_indication.assert_called_once()
- finished_params_from_callback = cast(
- TransactionFinishedParams,
- self.cfdp_user.transaction_finished_indication.call_args.args[0],
- )
- self.assertEqual(
- finished_params_from_callback.transaction_id, self.transaction_id
- )
- self.assertEqual(fsm_res.states.transaction_id, self.transaction_id)
- self.assertEqual(
- finished_params_from_callback.finished_params, expected_finished_params
- )
-
- def _generate_put_response_opts(self) -> TlvList:
- return [
- OriginatingTransactionId(
- TransactionId(ByteFieldU16(1), ByteFieldU16(2))
- ).to_generic_msg_to_user_tlv(),
- ProxyPutResponse(
- ProxyPutResponseParams(
- ConditionCode.NO_ERROR,
- DeliveryCode.DATA_COMPLETE,
- FileStatus.FILE_RETAINED,
- )
- ).to_generic_msg_to_user_tlv(),
- ]
-
- def _generate_metadata_only_metadata(
- self, options: Optional[TlvList]
- ) -> MetadataPdu:
- metadata_params = MetadataParams(
- checksum_type=NULL_CHECKSUM_U32,
- closure_requested=self.closure_requested,
- source_file_name=None,
- dest_file_name=None,
- file_size=0,
- )
- return MetadataPdu(
- params=metadata_params, pdu_conf=self.src_pdu_conf, options=options
- )
-
- def tearDown(self) -> None:
- if self.dest_file_path.exists():
- os.remove(self.dest_file_path)
- if self.src_file_path.exists():
- os.remove(self.src_file_path)
diff --git a/tests/cfdp/test_dest_handler_acked.py b/tests/cfdp/test_dest_handler_acked.py
deleted file mode 100644
index 5f502b8f..00000000
--- a/tests/cfdp/test_dest_handler_acked.py
+++ /dev/null
@@ -1,570 +0,0 @@
-import time
-import struct
-from typing import List, Tuple
-
-from spacepackets.cfdp import (
- NULL_CHECKSUM_U32,
- ConditionCode,
- DirectiveType,
- PduType,
- TransmissionMode,
-)
-from spacepackets.cfdp.pdu import (
- AckPdu,
- EofPdu,
- FinishedPdu,
- TransactionStatus,
- DeliveryCode,
- FileStatus,
- FinishedParams,
-)
-from spacepackets.crc import mkPredefinedCrcFun
-
-from .test_dest_handler import TestDestHandlerBase
-from tmtccmd.cfdp.defs import CfdpState
-from tmtccmd.cfdp.handler.dest import FsmResult, TransactionStep
-from tmtccmd.cfdp.user import MetadataRecvParams, TransactionFinishedParams
-
-
-class TestDestHandlerAcked(TestDestHandlerBase):
- def setUp(self) -> None:
- self.common_setup(TransmissionMode.ACKNOWLEDGED)
-
- def test_acked_empty_transfer(self):
- # Basic acknowledged empty file transfer.
- self._generic_regular_transfer_init(0)
- fsm_res = self._generic_insert_eof_pdu(0, NULL_CHECKSUM_U32)
- self._generic_eof_recv_indication_check(fsm_res)
- self._generic_verify_eof_ack_packet(fsm_res)
- fsm_res = self.dest_handler.state_machine()
- finished_pdu = self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, bytes())
- self._generic_insert_finished_pdu_ack(finished_pdu)
-
- def test_acked_small_file_transfer(self):
- file_content = "Hello World!".encode()
- with open(self.src_file_path, "wb") as of:
- of.write(file_content)
- crc32_func = mkPredefinedCrcFun("crc32")
- crc32 = struct.pack("!I", crc32_func(file_content))
- # Basic acknowledged empty file transfer.
- self._generic_regular_transfer_init(len(file_content))
- self._insert_file_segment(file_content, 0)
- fsm_res = self._generic_insert_eof_pdu(len(file_content), crc32)
- self._generic_eof_recv_indication_check(fsm_res)
- self._generic_verify_eof_ack_packet(fsm_res)
- fsm_res = self.dest_handler.state_machine()
- finished_pdu = self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, file_content)
- self._generic_insert_finished_pdu_ack(finished_pdu)
-
- def test_cancelled_file_transfer(self):
- file_content = "Hello World!".encode()
- with open(self.src_file_path, "wb") as of:
- of.write(file_content)
- # Basic acknowledged empty file transfer.
- self._generic_regular_transfer_init(len(file_content))
- # Cancel the transfer by sending an EOF PDU with the appropriate parameters.
- eof_pdu = EofPdu(
- file_size=0,
- file_checksum=NULL_CHECKSUM_U32,
- pdu_conf=self.src_pdu_conf,
- condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,
- )
- self.dest_handler.insert_packet(eof_pdu)
- fsm_res = self.dest_handler.state_machine()
- # Should contain an ACK PDU now.
- self._generic_verify_eof_ack_packet(
- fsm_res, condition_code_of_acked_pdu=ConditionCode.CANCEL_REQUEST_RECEIVED
- )
- fsm_res = self.dest_handler.state_machine()
- finished_pdu = self._generic_no_error_finished_pdu_check(
- fsm_res, expected_condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED
- )
- self._generic_verify_transfer_completion(
- fsm_res,
- expected_file_data=None,
- expected_finished_params=FinishedParams(
- condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,
- delivery_code=DeliveryCode.DATA_INCOMPLETE,
- file_status=FileStatus.FILE_RETAINED,
- ),
- )
- self._generic_insert_finished_pdu_ack(finished_pdu)
-
- def test_deferred_missing_file_segment_handling(self):
- file_content = "Hello World!".encode()
- with open(self.src_file_path, "wb") as of:
- of.write(file_content)
- crc32_func = mkPredefinedCrcFun("crc32")
- crc32 = struct.pack("!I", crc32_func(file_content))
- self._generic_regular_transfer_init(len(file_content))
- self._insert_file_segment(file_content[0:5], 0)
- fsm_res = self._generic_insert_eof_pdu(len(file_content), crc32)
- self._generic_eof_recv_indication_check(fsm_res)
- self._generic_verify_eof_ack_packet(fsm_res)
- self.dest_handler.state_machine()
- self._state_checker(
- fsm_res, 1, CfdpState.BUSY, TransactionStep.WAITING_FOR_MISSING_DATA
- )
- self.assertTrue(self.dest_handler.deferred_lost_segment_procedure_active)
- self.assertEqual(self.dest_handler.nak_activity_counter, 0)
- self._generic_verify_missing_segment_requested(
- 0, len(file_content), [(5, len(file_content))]
- )
- fsm_res = self._insert_file_segment(
- file_content[5:],
- 5,
- expected_packets=1,
- expected_step=TransactionStep.SENDING_FINISHED_PDU,
- )
- finished_pdu = self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, file_content)
- self._generic_insert_finished_pdu_ack(finished_pdu)
-
- def test_immediate_missing_file_seg_handling_0(self):
- file_content = "Hello World!".encode()
- with open(self.src_file_path, "wb") as of:
- of.write(file_content)
- crc32_func = mkPredefinedCrcFun("crc32")
- crc32 = struct.pack("!I", crc32_func(file_content))
- self._generic_regular_transfer_init(len(file_content))
- # A middle segment is missing now, with the expected lost segment tuple to be (3, 6). The
- # lost segment is immediately supplied.
- self._insert_file_segment(file_content[0:3], 0)
- self._insert_file_segment(file_content[6:], 6, 1)
- self._generic_verify_missing_segment_requested(0, len(file_content), [(3, 6)])
- # Insert the missing file content.
- self._insert_file_segment(file_content[3:6], 3)
- # All lost segments were delivered, regular transfer finish.
- fsm_res = self._generic_insert_eof_pdu(len(file_content), crc32)
- self._generic_eof_recv_indication_check(fsm_res)
- self._generic_verify_eof_ack_packet(fsm_res)
- fsm_res = self.dest_handler.state_machine()
- finished_pdu = self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, file_content)
- self._generic_insert_finished_pdu_ack(finished_pdu)
-
- def test_immediate_missing_file_seg_handling_1(self):
- file_content = "Hello World!".encode()
- with open(self.src_file_path, "wb") as of:
- of.write(file_content)
- crc32_func = mkPredefinedCrcFun("crc32")
- crc32 = struct.pack("!I", crc32_func(file_content))
- self._generic_regular_transfer_init(len(file_content))
- # Simulate the second segment being lost, with more than one segment following after that.
- self._insert_file_segment(file_content[0:2], 0)
- self._insert_file_segment(file_content[4:6], 4, 1)
- self._generic_verify_missing_segment_requested(0, 6, [(2, 4)])
- # Insert the last file segment.
- self._insert_file_segment(file_content[6:], 6)
- # Now insert the missing segment.
- self._insert_file_segment(file_content[2:4], 2)
- # All lost segments were delivered, regular transfer finish.
- fsm_res = self._generic_insert_eof_pdu(len(file_content), crc32)
- self._generic_eof_recv_indication_check(fsm_res)
- self._generic_verify_eof_ack_packet(fsm_res)
- fsm_res = self.dest_handler.state_machine()
- finished_pdu = self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, file_content)
- self._generic_insert_finished_pdu_ack(finished_pdu)
-
- def test_immediate_multi_missing_segment_handling(self):
- file_content = "Hello World!".encode()
- with open(self.src_file_path, "wb") as of:
- of.write(file_content)
- crc32_func = mkPredefinedCrcFun("crc32")
- crc32 = struct.pack("!I", crc32_func(file_content))
- self._generic_regular_transfer_init(len(file_content))
- self._insert_file_segment(file_content[0:2], 0)
- self._insert_file_segment(file_content[4:6], 4, 1)
- # First missing segment.
- self._generic_verify_missing_segment_requested(0, 6, [(2, 4)])
-
- # Second missing segment directly after that.
- self._insert_file_segment(file_content[8:], 8, 1)
- self._generic_verify_missing_segment_requested(0, len(file_content), [(6, 8)])
-
- # Supply the 2 missing file segments.
- self._insert_file_segment(file_content[2:4], 2)
- self._insert_file_segment(file_content[6:8], 6)
- # All lost segments were delivered, regular transfer finish.
- fsm_res = self._generic_insert_eof_pdu(len(file_content), crc32)
- self._generic_eof_recv_indication_check(fsm_res)
- self._generic_verify_eof_ack_packet(fsm_res)
- fsm_res = self.dest_handler.state_machine()
- finished_pdu = self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, file_content)
- self._generic_insert_finished_pdu_ack(finished_pdu)
-
- def test_immediate_missing_segment_also_rerequested_after_eof(self):
- file_content = "Hello World!".encode()
- with open(self.src_file_path, "wb") as of:
- of.write(file_content)
- crc32_func = mkPredefinedCrcFun("crc32")
- crc32 = struct.pack("!I", crc32_func(file_content))
- self._generic_regular_transfer_init(len(file_content))
- # Missing middle segment
- self._insert_file_segment(file_content[0:2], 0)
- self._insert_file_segment(file_content[6:], 6, 1)
- # Missing segment immediately re-requested.
- self._generic_verify_missing_segment_requested(0, len(file_content), [(2, 6)])
-
- # All lost segments were delivered, regular transfer finish.
- fsm_res = self._generic_insert_eof_pdu(len(file_content), crc32)
- self._generic_eof_recv_indication_check(fsm_res)
- self._generic_verify_eof_ack_packet(fsm_res)
-
- self.dest_handler.state_machine()
- self._state_checker(
- fsm_res, 1, CfdpState.BUSY, TransactionStep.WAITING_FOR_MISSING_DATA
- )
- self.assertTrue(self.dest_handler.deferred_lost_segment_procedure_active)
- self.assertEqual(self.dest_handler.nak_activity_counter, 0)
- self._generic_verify_missing_segment_requested(0, len(file_content), [(2, 6)])
- fsm_res = self._insert_file_segment(
- file_content[2:6],
- 2,
- 1,
- expected_step=TransactionStep.SENDING_FINISHED_PDU,
- )
- finished_pdu = self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, file_content)
- self._generic_insert_finished_pdu_ack(finished_pdu)
-
- def test_multi_segment_missing_deferred_handling(self):
- file_content = "Hello World!".encode()
- with open(self.src_file_path, "wb") as of:
- of.write(file_content)
- crc32_func = mkPredefinedCrcFun("crc32")
- crc32 = struct.pack("!I", crc32_func(file_content))
- self._generic_regular_transfer_init(len(file_content))
- self._insert_file_segment(file_content[0:2], 0)
- self._insert_file_segment(file_content[4:6], 4, 1)
- # First missing segment.
- self._generic_verify_missing_segment_requested(0, 6, [(2, 4)])
-
- # Second missing segment directly after that.
- self._insert_file_segment(file_content[8:], 8, 1)
- self._generic_verify_missing_segment_requested(0, len(file_content), [(6, 8)])
-
- fsm_res = self._generic_insert_eof_pdu(len(file_content), crc32)
- self._generic_eof_recv_indication_check(fsm_res)
- self._generic_verify_eof_ack_packet(fsm_res)
-
- self.dest_handler.state_machine()
- self._state_checker(
- fsm_res, 1, CfdpState.BUSY, TransactionStep.WAITING_FOR_MISSING_DATA
- )
- self.assertTrue(self.dest_handler.deferred_lost_segment_procedure_active)
- self.assertEqual(self.dest_handler.nak_activity_counter, 0)
- # We now receive a NAK sequence with both missing file segments.
- self._generic_verify_missing_segment_requested(
- 0, len(file_content), [(2, 4), (6, 8)]
- )
- # We insert both missing file segments.
- fsm_res = self._insert_file_segment(
- file_content[2:4],
- 2,
- expected_packets=0,
- expected_step=TransactionStep.WAITING_FOR_MISSING_DATA,
- )
- fsm_res = self._insert_file_segment(
- file_content[6:8],
- 6,
- expected_packets=1,
- expected_step=TransactionStep.SENDING_FINISHED_PDU,
- )
- # Done.
- finished_pdu = self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, file_content)
- self._generic_insert_finished_pdu_ack(finished_pdu)
-
- def test_missing_metadata_pdu(self):
- file_content = "Hello World!".encode()
- with open(self.src_file_path, "wb") as of:
- of.write(file_content)
- crc32_func = mkPredefinedCrcFun("crc32")
- crc32 = struct.pack("!I", crc32_func(file_content))
- self._insert_file_segment(
- file_content[0:2],
- 0,
- expected_packets=1,
- check_indication=False,
- expected_step=TransactionStep.WAITING_FOR_METADATA,
- )
- next_pdu = self.dest_handler.get_next_packet()
- self.assertIsNotNone(next_pdu)
- self.assertEqual(next_pdu.pdu_type, PduType.FILE_DIRECTIVE)
- self.assertEqual(next_pdu.pdu_directive_type, DirectiveType.NAK_PDU)
- nak_pdu = next_pdu.to_nak_pdu()
- self.assertEqual(nak_pdu.start_of_scope, 0)
- self.assertEqual(nak_pdu.end_of_scope, 2)
- # Metadata and the segment we just sent are immediately re-requested.
- self.assertEqual(nak_pdu.segment_requests, [(0, 0), (0, 2)])
-
- self._generic_transfer_init(
- len(file_content),
- expected_init_packets=0,
- expected_init_state=CfdpState.BUSY,
- expected_init_step=TransactionStep.WAITING_FOR_METADATA,
- )
- self._insert_file_segment(
- file_content[0:2],
- 0,
- )
- self._insert_file_segment(
- file_content[2:],
- 2,
- )
- # All lost segments were delivered, regular transfer finish.
- fsm_res = self._generic_insert_eof_pdu(len(file_content), crc32)
- self._generic_eof_recv_indication_check(fsm_res)
- self._generic_verify_eof_ack_packet(fsm_res)
- fsm_res = self.dest_handler.state_machine()
- finished_pdu = self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, file_content)
- self._generic_insert_finished_pdu_ack(finished_pdu)
-
- def test_metadata_eof_only_missing_metadata(self):
- fsm_res = self._generic_insert_eof_pdu(0, NULL_CHECKSUM_U32)
- self._generic_eof_recv_indication_check(fsm_res)
- self._generic_verify_eof_ack_packet(fsm_res)
- self.dest_handler.state_machine()
- self._state_checker(
- fsm_res, 1, CfdpState.BUSY, TransactionStep.WAITING_FOR_METADATA
- )
- self._generic_verify_missing_segment_requested(0, 0, [(0, 0)])
- fsm_res = self._generic_transfer_init(
- 0,
- expected_init_packets=0,
- expected_init_state=CfdpState.BUSY,
- expected_init_step=TransactionStep.WAITING_FOR_METADATA,
- )
- self._state_checker(
- fsm_res, 1, CfdpState.BUSY, TransactionStep.SENDING_FINISHED_PDU
- )
- finished_pdu = self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, bytes())
- self._generic_insert_finished_pdu_ack(finished_pdu)
-
- def _generic_deferred_lost_segment_handling_with_timeout(self, file_content: bytes):
- with open(self.src_file_path, "wb") as of:
- of.write(file_content)
- crc32_func = mkPredefinedCrcFun("crc32")
- crc32 = struct.pack("!I", crc32_func(file_content))
- self._generic_regular_transfer_init(len(file_content))
- self._insert_file_segment(file_content[0:2], 0)
- self._insert_file_segment(file_content[4:6], 4, 1)
- # First missing segment.
- self._generic_verify_missing_segment_requested(0, 6, [(2, 4)])
-
- # Second missing segment directly after that.
- self._insert_file_segment(file_content[8:], 8, 1)
- self._generic_verify_missing_segment_requested(0, len(file_content), [(6, 8)])
-
- # This should trigger deferred EOF handling.
- fsm_res = self._generic_insert_eof_pdu(len(file_content), crc32)
- self._generic_eof_recv_indication_check(fsm_res)
- self._generic_verify_eof_ack_packet(fsm_res)
- self.dest_handler.state_machine()
- self._state_checker(
- fsm_res, 1, CfdpState.BUSY, TransactionStep.WAITING_FOR_MISSING_DATA
- )
- self.assertTrue(self.dest_handler.deferred_lost_segment_procedure_active)
- self.assertEqual(self.dest_handler.nak_activity_counter, 0)
- # We now receive a NAK sequence with both missing file segments.
- self._generic_verify_missing_segment_requested(
- 0, len(file_content), [(2, 4), (6, 8)]
- )
- time.sleep(self.timeout_nak_procedure_seconds * 1.1)
- self.dest_handler.state_machine()
- self.assertTrue(self.dest_handler.deferred_lost_segment_procedure_active)
- self.assertEqual(self.dest_handler.nak_activity_counter, 1)
- # We now receive a NAK sequence with both missing file segments.
- self._generic_verify_missing_segment_requested(
- 0, len(file_content), [(2, 4), (6, 8)]
- )
-
- def test_deferred_lost_segment_handling_after_timeout(self):
- file_content = "Hello World!".encode()
- self._generic_deferred_lost_segment_handling_with_timeout(file_content)
- time.sleep(self.timeout_nak_procedure_seconds * 1.1)
- fsm_res = self.dest_handler.state_machine()
- self._generic_finished_pdu_with_error_check(
- fsm_res,
- cond_code=ConditionCode.NAK_LIMIT_REACHED,
- delivery_code=DeliveryCode.DATA_INCOMPLETE,
- file_status=FileStatus.FILE_RETAINED,
- )
-
- def test_deferred_lost_segment_handling_after_timeout_activity_reset(self):
- file_content = "Hello World!".encode()
- self._generic_deferred_lost_segment_handling_with_timeout(file_content)
- # Insert one segment, which should reset the NAK activity parameters.
- self._insert_file_segment(
- file_content[2:4],
- 2,
- expected_packets=0,
- expected_step=TransactionStep.WAITING_FOR_MISSING_DATA,
- )
- self.dest_handler.state_machine()
- # Now that we inserted a packet, the NAK activity counter should be reset.
- self.assertTrue(self.dest_handler.deferred_lost_segment_procedure_active)
- self.assertEqual(self.dest_handler.nak_activity_counter, 0)
- self._state_checker(
- None, 0, CfdpState.BUSY, TransactionStep.WAITING_FOR_MISSING_DATA
- )
- time.sleep(self.timeout_nak_procedure_seconds * 1.1)
- self.dest_handler.state_machine()
- self.assertTrue(self.dest_handler.deferred_lost_segment_procedure_active)
- self.assertEqual(self.dest_handler.nak_activity_counter, 1)
- # We now receive a NAK sequence with the only file segment missing
- self._generic_verify_missing_segment_requested(0, len(file_content), [(6, 8)])
- fsm_res = self._insert_file_segment(
- file_content[6:8],
- 6,
- expected_packets=1,
- expected_step=TransactionStep.SENDING_FINISHED_PDU,
- )
- finished_pdu = self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, file_content)
- self._generic_insert_finished_pdu_ack(finished_pdu)
-
- def test_positive_ack_procedure_finished_pdu(self):
- # Basic acknowledged empty file transfer.
- self._generic_regular_transfer_init(0)
- fsm_res = self._generic_insert_eof_pdu(0, NULL_CHECKSUM_U32)
- self._generic_eof_recv_indication_check(fsm_res)
- self._generic_verify_eof_ack_packet(fsm_res)
- fsm_res = self.dest_handler.state_machine()
- self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, bytes())
- time.sleep(self.timeout_positive_ack_procedure_seconds * 1.1)
- fsm_res = self.dest_handler.state_machine()
- self.assertEqual(self.dest_handler.positive_ack_counter, 1)
- self._state_checker(
- fsm_res, 1, CfdpState.BUSY, TransactionStep.WAITING_FOR_FINISHED_ACK
- )
- self._generic_no_error_finished_pdu_check(
- fsm_res, TransactionStep.WAITING_FOR_FINISHED_ACK
- )
- fsm_res = self.dest_handler.state_machine()
- self.assertEqual(self.dest_handler.positive_ack_counter, 1)
- time.sleep(self.timeout_positive_ack_procedure_seconds * 1.1)
- fsm_res = self.dest_handler.state_machine()
- self._generic_finished_pdu_with_error_check(
- fsm_res,
- ConditionCode.POSITIVE_ACK_LIMIT_REACHED,
- DeliveryCode.DATA_COMPLETE,
- FileStatus.FILE_RETAINED,
- )
-
- def test_metadata_only_transfer(self):
- options = self._generate_put_response_opts()
- metadata_pdu = self._generate_metadata_only_metadata(options)
- self.dest_handler.insert_packet(metadata_pdu)
- fsm_res = self.dest_handler.state_machine()
- # Done immediately. The only thing we need to do is check the two user indications.
- self.cfdp_user.metadata_recv_indication.assert_called_once()
- self.cfdp_user.metadata_recv_indication.assert_called_with(
- MetadataRecvParams(
- self.transaction_id,
- self.src_pdu_conf.source_entity_id,
- None,
- None,
- None,
- options,
- )
- )
- self.cfdp_user.transaction_finished_indication.assert_called_once()
- self.cfdp_user.transaction_finished_indication.assert_called_with(
- TransactionFinishedParams(
- self.transaction_id,
- FinishedParams(
- delivery_code=DeliveryCode.DATA_COMPLETE,
- condition_code=ConditionCode.NO_ERROR,
- file_status=FileStatus.FILE_STATUS_UNREPORTED,
- ),
- )
- )
- finished_pdu = self._generic_no_error_finished_pdu_check(
- fsm_res, expected_file_status=FileStatus.FILE_STATUS_UNREPORTED
- )
- self._generic_verify_transfer_completion(
- fsm_res,
- expected_file_data=None,
- expected_finished_params=FinishedParams(
- condition_code=ConditionCode.NO_ERROR,
- file_status=FileStatus.FILE_STATUS_UNREPORTED,
- delivery_code=DeliveryCode.DATA_COMPLETE,
- ),
- )
- self._generic_insert_finished_pdu_ack(finished_pdu)
-
- def _generic_finished_pdu_with_error_check(
- self,
- fsm_res: FsmResult,
- cond_code: ConditionCode,
- delivery_code: DeliveryCode,
- file_status: FileStatus,
- ):
- self._state_checker(
- fsm_res, 1, CfdpState.BUSY, TransactionStep.SENDING_FINISHED_PDU
- )
- next_pdu = self.dest_handler.get_next_packet()
- self.assertIsNotNone(next_pdu)
- self.assertEqual(next_pdu.pdu_type, PduType.FILE_DIRECTIVE)
- self.assertEqual(next_pdu.pdu_directive_type, DirectiveType.FINISHED_PDU)
- finished_pdu = next_pdu.to_finished_pdu()
- self.assertEqual(finished_pdu.condition_code, cond_code)
- self.assertEqual(finished_pdu.delivery_code, delivery_code)
- self.assertEqual(finished_pdu.file_status, file_status)
-
- def _generic_verify_missing_segment_requested(
- self,
- start_of_scope: int,
- end_of_scope: int,
- segment_reqs: List[Tuple[int, int]],
- ):
- next_pdu = self.dest_handler.get_next_packet()
- assert next_pdu is not None
- self.assertEqual(next_pdu.pdu_type, PduType.FILE_DIRECTIVE)
- self.assertEqual(next_pdu.pdu_directive_type, DirectiveType.NAK_PDU)
- nak_pdu = next_pdu.to_nak_pdu()
- self.assertEqual(nak_pdu.start_of_scope, start_of_scope)
- self.assertEqual(nak_pdu.end_of_scope, end_of_scope)
- self.assertEqual(nak_pdu.segment_requests, segment_reqs)
-
- def _generic_verify_eof_ack_packet(
- self,
- fsm_res: FsmResult,
- condition_code_of_acked_pdu: ConditionCode = ConditionCode.NO_ERROR,
- ):
- self._state_checker(
- fsm_res,
- 1,
- CfdpState.BUSY,
- TransactionStep.SENDING_EOF_ACK_PDU,
- )
- next_pdu = self.dest_handler.get_next_packet()
- assert next_pdu is not None
- self.assertEqual(next_pdu.pdu_type, PduType.FILE_DIRECTIVE)
- self.assertEqual(next_pdu.pdu_directive_type, DirectiveType.ACK_PDU)
- ack_pdu = next_pdu.to_ack_pdu()
- self.assertEqual(ack_pdu.directive_code_of_acked_pdu, DirectiveType.EOF_PDU)
- self.assertEqual(
- ack_pdu.condition_code_of_acked_pdu, condition_code_of_acked_pdu
- )
- self.assertEqual(ack_pdu.transaction_status, TransactionStatus.ACTIVE)
-
- def _generic_insert_finished_pdu_ack(self, finished_pdu: FinishedPdu):
- ack_pdu = AckPdu(
- finished_pdu.pdu_header.pdu_conf,
- DirectiveType.FINISHED_PDU,
- finished_pdu.condition_code,
- TransactionStatus.ACTIVE,
- )
- self.dest_handler.insert_packet(ack_pdu)
- fsm_res = self.dest_handler.state_machine()
- self._state_checker(fsm_res, 0, CfdpState.IDLE, TransactionStep.IDLE)
diff --git a/tests/cfdp/test_dest_handler_naked.py b/tests/cfdp/test_dest_handler_naked.py
deleted file mode 100644
index d2a41126..00000000
--- a/tests/cfdp/test_dest_handler_naked.py
+++ /dev/null
@@ -1,365 +0,0 @@
-import os
-import random
-import struct
-import sys
-import time
-from typing import cast
-
-from crcmod.predefined import mkPredefinedCrcFun
-from spacepackets.cfdp import (
- NULL_CHECKSUM_U32,
- ChecksumType,
- ConditionCode,
- DeliveryCode,
- FileStatus,
- TransmissionMode,
-)
-from spacepackets.cfdp.pdu import (
- EofPdu,
- FileDataPdu,
- MetadataParams,
- MetadataPdu,
- FinishedParams,
-)
-from spacepackets.cfdp.pdu.file_data import FileDataParams
-
-from tests.cfdp.common import CheckTimerProviderForTest
-from tests.cfdp.test_dest_handler import FileInfo, TestDestHandlerBase
-from tmtccmd.cfdp import (
- RemoteEntityCfgTable,
-)
-from tmtccmd.cfdp.defs import CfdpState
-from tmtccmd.cfdp.exceptions import NoRemoteEntityCfgFound
-from tmtccmd.cfdp.handler.dest import (
- DestHandler,
- PduIgnoredForDest,
- TransactionStep,
-)
-from tmtccmd.cfdp.user import MetadataRecvParams, TransactionFinishedParams
-
-
-class TestCfdpDestHandler(TestDestHandlerBase):
- def setUp(self) -> None:
- self.common_setup(TransmissionMode.UNACKNOWLEDGED)
-
- def _generic_empty_file_test(self):
- self._generic_regular_transfer_init(0)
- fsm_res = self._generic_insert_eof_pdu(0, NULL_CHECKSUM_U32)
- self._generic_eof_recv_indication_check(fsm_res)
- if self.closure_requested:
- self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, bytes())
-
- def test_empty_file_reception(self):
- self._generic_empty_file_test()
-
- def test_empty_file_reception_with_closure(self):
- self.closure_requested = True
- self._generic_empty_file_test()
-
- def _generic_small_file_test(self):
- data = "Hello World\n".encode()
- with open(self.src_file_path, "wb") as of:
- of.write(data)
- crc32_func = mkPredefinedCrcFun("crc32")
- crc32 = struct.pack("!I", crc32_func(data))
- file_size = self.src_file_path.stat().st_size
- self._generic_regular_transfer_init(
- file_size=file_size,
- )
- self._insert_file_segment(segment=data, offset=0)
- fsm_res = self._generic_insert_eof_pdu(file_size, crc32)
- self._generic_eof_recv_indication_check(fsm_res)
- if self.closure_requested:
- self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, data)
-
- def test_small_file_reception_no_closure(self):
- self._generic_small_file_test()
-
- def test_small_file_reception_with_closure(self):
- self.closure_requested = True
- self._generic_small_file_test()
-
- def _generic_larger_file_reception_test(self):
- # This tests generates two file data PDUs, but the second one does not have a
- # full segment length
- file_info = self._random_data_two_file_segments()
- self._state_checker(None, False, CfdpState.IDLE, TransactionStep.IDLE)
- self._generic_regular_transfer_init(
- file_size=file_info.file_size,
- )
- self._insert_file_segment(file_info.rand_data[0 : self.file_segment_len], 0)
- self._insert_file_segment(
- file_info.rand_data[self.file_segment_len :], offset=self.file_segment_len
- )
- fsm_res = self._generic_insert_eof_pdu(file_info.file_size, file_info.crc32)
- self._generic_eof_recv_indication_check(fsm_res)
- if self.closure_requested:
- self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(fsm_res, file_info.rand_data)
-
- def test_larger_file_reception(self):
- self._generic_larger_file_reception_test()
-
- def test_larger_file_reception_with_closure(self):
- self.closure_requested = True
- self._generic_larger_file_reception_test()
-
- def test_remote_cfg_does_not_exist(self):
- # Re-create empty table
- self.remote_cfg_table = RemoteEntityCfgTable()
- self.dest_handler = DestHandler(
- self.local_cfg,
- self.cfdp_user,
- self.remote_cfg_table,
- CheckTimerProviderForTest(5),
- )
- metadata_params = MetadataParams(
- checksum_type=ChecksumType.NULL_CHECKSUM,
- closure_requested=False,
- source_file_name=self.src_file_path.as_posix(),
- dest_file_name=self.dest_file_path.as_posix(),
- file_size=0,
- )
- file_transfer_init = MetadataPdu(
- params=metadata_params, pdu_conf=self.src_pdu_conf
- )
- self._state_checker(None, False, CfdpState.IDLE, TransactionStep.IDLE)
- with self.assertRaises(NoRemoteEntityCfgFound):
- self.dest_handler.insert_packet(file_transfer_init)
-
- def test_check_timer_mechanism(self):
- file_data = "Hello World\n".encode()
- self._generic_check_limit_test(file_data)
- fd_params = FileDataParams(
- file_data=file_data,
- offset=0,
- )
- file_data_pdu = FileDataPdu(params=fd_params, pdu_conf=self.src_pdu_conf)
- self.dest_handler.insert_packet(file_data_pdu)
- fsm_res = self.dest_handler.state_machine()
- self._state_checker(
- fsm_res,
- False,
- CfdpState.BUSY,
- TransactionStep.RECV_FILE_DATA_WITH_CHECK_LIMIT_HANDLING,
- )
- self.assertFalse(self.dest_handler.packets_ready)
- time.sleep(self.timeout_check_limit_handling_ms * 1.15 / 1000.0)
- fsm_res = self.dest_handler.state_machine()
- self._state_checker(
- fsm_res,
- False,
- CfdpState.IDLE,
- TransactionStep.IDLE,
- )
-
- def test_cancelled_transfer(self):
- data = "Hello World\n".encode()
- with open(self.src_file_path, "wb") as of:
- of.write(data)
- file_size = self.src_file_path.stat().st_size
- self._generic_regular_transfer_init(
- file_size=file_size,
- )
- self._insert_file_segment(segment=data, offset=0)
- # Cancel the transfer by sending an EOF PDU with the appropriate parameters.
- eof_pdu = EofPdu(
- file_size=0,
- file_checksum=NULL_CHECKSUM_U32,
- pdu_conf=self.src_pdu_conf,
- condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,
- )
- self.dest_handler.insert_packet(eof_pdu)
- fsm_res = self.dest_handler.state_machine()
- self._generic_eof_recv_indication_check(fsm_res)
- if self.closure_requested:
- self._generic_no_error_finished_pdu_check(fsm_res)
- self._generic_verify_transfer_completion(
- fsm_res,
- expected_file_data=None,
- expected_finished_params=FinishedParams(
- condition_code=ConditionCode.CANCEL_REQUEST_RECEIVED,
- delivery_code=DeliveryCode.DATA_INCOMPLETE,
- file_status=FileStatus.FILE_RETAINED,
- ),
- )
-
- def test_check_limit_reached(self):
- data = "Hello World\n".encode()
- self._generic_check_limit_test(data)
- transaction_id = self.dest_handler.transaction_id
- assert transaction_id is not None
- # Check counter should be incremented by one.
- time.sleep(self.timeout_check_limit_handling_ms * 1.25 / 1000.0)
- fsm_res = self.dest_handler.state_machine()
- self._state_checker(
- fsm_res,
- 0,
- CfdpState.BUSY,
- TransactionStep.RECV_FILE_DATA_WITH_CHECK_LIMIT_HANDLING,
- )
- self.assertEqual(self.dest_handler.current_check_counter, 1)
- # After this delay, the expiry limit (2) is reached and a check limit fault
- # is declared
- time.sleep(self.timeout_check_limit_handling_ms * 1.25 / 1000.0)
- fsm_res = self.dest_handler.state_machine()
- self.assertEqual(self.dest_handler.current_check_counter, 0)
- self._state_checker(
- fsm_res,
- 0,
- CfdpState.IDLE,
- TransactionStep.IDLE,
- )
- self.fault_handler.notice_of_cancellation_cb.assert_called_once()
- self.fault_handler.notice_of_cancellation_cb.assert_called_with(
- transaction_id, ConditionCode.CHECK_LIMIT_REACHED, 0
- )
- self.cfdp_user.transaction_finished_indication.assert_called_once()
- self.cfdp_user.transaction_finished_indication.assert_called_with(
- TransactionFinishedParams(
- transaction_id,
- FinishedParams(
- condition_code=ConditionCode.CHECK_LIMIT_REACHED,
- delivery_code=DeliveryCode.DATA_INCOMPLETE,
- file_status=FileStatus.FILE_RETAINED,
- ),
- )
- )
-
- def test_file_is_overwritten(self):
- with open(self.dest_file_path, "w") as of:
- of.write("This file will be truncated")
- self.test_small_file_reception_no_closure()
-
- def test_file_data_pdu_before_metadata_is_discarded(self):
- file_info = self._random_data_two_file_segments()
- with self.assertRaises(PduIgnoredForDest):
- # Pass file data PDU first. Will be discarded
- fsm_res = self._insert_file_segment(
- file_info.rand_data[0 : self.file_segment_len], 0
- )
- self._state_checker(fsm_res, False, CfdpState.IDLE, TransactionStep.IDLE)
- self._generic_regular_transfer_init(
- file_size=file_info.file_size,
- )
- fsm_res = self._insert_file_segment(
- segment=file_info.rand_data[: self.file_segment_len],
- offset=0,
- )
- fsm_res = self._insert_file_segment(
- segment=file_info.rand_data[self.file_segment_len :],
- offset=self.file_segment_len,
- )
- eof_pdu = EofPdu(
- file_size=file_info.file_size,
- file_checksum=file_info.crc32,
- pdu_conf=self.src_pdu_conf,
- )
- self.dest_handler.insert_packet(eof_pdu)
- fsm_res = self.dest_handler.state_machine()
- self.cfdp_user.transaction_finished_indication.assert_called_once()
- finished_args = cast(
- TransactionFinishedParams,
- self.cfdp_user.transaction_finished_indication.call_args.args[0],
- )
- # At least one segment was stored
- self.assertEqual(
- finished_args.finished_params.file_status,
- FileStatus.FILE_RETAINED,
- )
- self.assertEqual(
- finished_args.finished_params.condition_code,
- ConditionCode.NO_ERROR,
- )
- self._state_checker(fsm_res, False, CfdpState.IDLE, TransactionStep.IDLE)
-
- def test_metadata_only_transfer(self):
- options = self._generate_put_response_opts()
- metadata_pdu = self._generate_metadata_only_metadata(options)
- self.dest_handler.insert_packet(metadata_pdu)
- fsm_res = self.dest_handler.state_machine()
- # Done immediately. The only thing we need to do is check the two user indications.
- self.cfdp_user.metadata_recv_indication.assert_called_once()
- self.cfdp_user.metadata_recv_indication.assert_called_with(
- MetadataRecvParams(
- self.transaction_id,
- self.src_pdu_conf.source_entity_id,
- None,
- None,
- None,
- options,
- )
- )
- self.cfdp_user.transaction_finished_indication.assert_called_once()
- self.cfdp_user.transaction_finished_indication.assert_called_with(
- TransactionFinishedParams(
- self.transaction_id,
- FinishedParams(
- condition_code=ConditionCode.NO_ERROR,
- file_status=FileStatus.FILE_STATUS_UNREPORTED,
- delivery_code=DeliveryCode.DATA_COMPLETE,
- ),
- )
- )
- self._state_checker(fsm_res, 0, CfdpState.IDLE, TransactionStep.IDLE)
-
- def test_permission_error(self):
- with open(self.src_file_path, "w") as of:
- of.write("Hello World\n")
- self.src_file_path.chmod(0o444)
- # TODO: This will cause permission errors, but the error handling for this has not been
- # implemented properly
- """
- file_size = src_file.stat().st_size
- self._source_simulator_transfer_init_with_metadata(
- checksum=ChecksumTypes.CRC_32,
- file_size=file_size,
- file_path=src_file.as_posix(),
- )
- with open(src_file, "rb") as rf:
- read_data = rf.read()
- fd_params = FileDataParams(file_data=read_data, offset=0)
- file_data_pdu = FileDataPdu(params=fd_params, pdu_conf=self.src_pdu_conf)
- self.dest_handler.pass_packet(file_data_pdu)
- fsm_res = self.dest_handler.state_machine()
- self._state_checker(
- fsm_res, CfdpStates.BUSY_CLASS_1_NACKED, TransactionStep.RECEIVING_FILE_DATA
- )
- """
- self.src_file_path.chmod(0o777)
-
- def _random_data_two_file_segments(self):
- if sys.version_info >= (3, 9):
- rand_data = random.randbytes(round(self.file_segment_len * 1.3))
- else:
- rand_data = os.urandom(round(self.file_segment_len * 1.3))
- file_size = len(rand_data)
- crc32_func = mkPredefinedCrcFun("crc32")
- crc32 = struct.pack("!I", crc32_func(rand_data))
- return FileInfo(file_size=file_size, crc32=crc32, rand_data=rand_data)
-
- def _generic_check_limit_test(self, file_data: bytes):
- with open(self.src_file_path, "wb") as of:
- of.write(file_data)
- crc32_func = mkPredefinedCrcFun("crc32")
- crc32 = struct.pack("!I", crc32_func(file_data))
- file_size = self.src_file_path.stat().st_size
- self._generic_regular_transfer_init(
- file_size=file_size,
- )
- eof_pdu = EofPdu(
- file_size=file_size,
- file_checksum=crc32,
- pdu_conf=self.src_pdu_conf,
- )
- self.dest_handler.insert_packet(eof_pdu)
- fsm_res = self.dest_handler.state_machine()
- self._state_checker(
- fsm_res,
- False,
- CfdpState.BUSY,
- TransactionStep.RECV_FILE_DATA_WITH_CHECK_LIMIT_HANDLING,
- )
- self._generic_eof_recv_indication_check(fsm_res)
diff --git a/tests/cfdp/test_filestore.py b/tests/cfdp/test_filestore.py
deleted file mode 100644
index 3a6e392b..00000000
--- a/tests/cfdp/test_filestore.py
+++ /dev/null
@@ -1,97 +0,0 @@
-import os.path
-from pathlib import Path
-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")
- self.filestore = HostFilestore()
-
- def test_creation(self):
- res = self.filestore.create_file(self.test_file_name_0)
- self.assertTrue(res == FilestoreResult.CREATE_SUCCESS)
- self.assertTrue(self.test_file_name_0.exists())
- res = self.filestore.create_file(self.test_file_name_0)
- self.assertEqual(res, FilestoreResult.CREATE_NOT_ALLOWED)
-
- res = self.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 = self.filestore.delete_file(self.test_file_name_0)
- self.assertTrue(res == FilestoreResult.DELETE_FILE_DOES_NOT_EXIST)
-
- def test_rename(self):
- self.filestore.create_file(self.test_file_name_0)
- res = self.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 = self.filestore.delete_file(self.test_file_name_1)
- self.assertTrue(res == FilestoreResult.DELETE_SUCCESS)
-
- def test_create_dir(self):
- res = self.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 = self.filestore.create_directory(self.test_file_name_0)
- self.assertTrue(res == FilestoreResult.CREATE_DIR_CAN_NOT_BE_CREATED)
-
- res = self.filestore.delete_file(self.test_file_name_0)
- self.assertTrue(res == FilestoreResult.DELETE_NOT_ALLOWED)
- res = self.filestore.remove_directory(self.test_file_name_0)
- self.assertTrue(res == FilestoreResult.REMOVE_DIR_SUCCESS)
-
- def test_read_file(self):
- file_data = "Hello World".encode()
- with open(self.test_file_name_0, "wb") as of:
- of.write(file_data)
- data = self.filestore.read_data(self.test_file_name_0, 0)
- self.assertEqual(data, file_data)
-
- def test_read_opened_file(self):
- file_data = "Hello World".encode()
- with open(self.test_file_name_0, "wb") as of:
- of.write(file_data)
- with open(self.test_file_name_0, "rb") as rf:
- data = self.filestore.read_from_opened_file(rf, 0, len(file_data))
- self.assertEqual(data, file_data)
-
- def test_write_file(self):
- file_data = "Hello World".encode()
- self.filestore.create_file(self.test_file_name_0)
- self.filestore.write_data(self.test_file_name_0, file_data, 0)
- with open(self.test_file_name_0, "rb") as rf:
- self.assertEqual(rf.read(), file_data)
-
- def test_replace_file(self):
- file_data = "Hello World".encode()
- self.filestore.create_file(self.test_file_name_0)
- self.filestore.write_data(self.test_file_name_0, file_data, 0)
- with open(self.test_file_name_1, "w"):
- pass
- self.filestore.replace_file(self.test_file_name_1, self.test_file_name_0)
- self.assertEqual(self.filestore.read_data(self.test_file_name_1, 0), file_data)
-
- 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/cfdp/test_lost_seg_tracker.py b/tests/cfdp/test_lost_seg_tracker.py
deleted file mode 100644
index ba5a672c..00000000
--- a/tests/cfdp/test_lost_seg_tracker.py
+++ /dev/null
@@ -1,83 +0,0 @@
-from unittest import TestCase
-
-from tmtccmd.cfdp.handler.dest import LostSegmentTracker
-
-
-class TestLostSegmentTracker(TestCase):
- def setUp(self) -> None:
- self.tracker = LostSegmentTracker()
-
- def test_basic(self):
- self.assertEqual(self.tracker.lost_segments, {})
- self.tracker.add_lost_segment((0, 500))
- seg_end = self.tracker.lost_segments[0]
- self.assertEqual(seg_end, 500)
-
- def test_coalesence_0(self):
- self.tracker.add_lost_segment((500, 1000))
- self.tracker.add_lost_segment((1000, 1500))
- self.tracker.coalesce_lost_segments()
- self.assertEqual(len(self.tracker.lost_segments), 1)
- seg_end = self.tracker.lost_segments[500]
- self.assertEqual(seg_end, 1500)
-
- def test_coalesence_1(self):
- self.tracker.add_lost_segment((500, 1000))
- self.tracker.add_lost_segment((1000, 1500))
- self.tracker.add_lost_segment((1500, 1700))
- self.tracker.coalesce_lost_segments()
- self.assertEqual(len(self.tracker.lost_segments), 1)
- seg_end = self.tracker.lost_segments[500]
- self.assertEqual(seg_end, 1700)
-
- def test_coalesence_2(self):
- self.tracker.add_lost_segment((500, 1000))
- self.tracker.add_lost_segment((1100, 1200))
- self.tracker.coalesce_lost_segments()
- self.assertEqual(len(self.tracker.lost_segments), 2)
- self.assertEqual(self.tracker.lost_segments, {500: 1000, 1100: 1200})
-
- def test_removal_0(self):
- self.tracker.add_lost_segment((0, 500))
- self.assertTrue(self.tracker.remove_lost_segment((0, 500)))
- self.assertEqual(self.tracker.lost_segments, {})
-
- def test_removal_1(self):
- self.tracker.add_lost_segment((0, 500))
- self.assertTrue(self.tracker.remove_lost_segment((0, 200)))
- self.assertEqual(self.tracker.lost_segments, {200: 500})
-
- def test_removal_2(self):
- self.tracker.add_lost_segment((0, 500))
- self.assertTrue(self.tracker.remove_lost_segment((300, 500)))
- self.assertEqual(self.tracker.lost_segments, {0: 300})
-
- def test_removal_3(self):
- self.tracker.add_lost_segment((0, 500))
- self.assertTrue(self.tracker.remove_lost_segment((300, 400)))
- self.assertEqual(self.tracker.lost_segments, {0: 300, 400: 500})
-
- def test_noop_removal_0(self):
- self.tracker.add_lost_segment((0, 500))
- self.assertFalse(self.tracker.remove_lost_segment((500, 1000)))
- self.assertEqual(self.tracker.lost_segments, {0: 500})
-
- def test_noop_removal_1(self):
- self.tracker.add_lost_segment((0, 500))
- self.assertFalse(self.tracker.remove_lost_segment((0, 0)))
- self.assertEqual(self.tracker.lost_segments, {0: 500})
-
- def test_noop_removal_2(self):
- self.tracker.add_lost_segment((0, 500))
- self.assertFalse(self.tracker.remove_lost_segment((500, 600)))
- self.assertEqual(self.tracker.lost_segments, {0: 500})
-
- def test_invalid_removal_0(self):
- self.tracker.add_lost_segment((0, 500))
- with self.assertRaises(ValueError):
- self.tracker.remove_lost_segment((0, 600))
-
- def test_invalid_removal_1(self):
- self.tracker.add_lost_segment((0, 500))
- with self.assertRaises(ValueError):
- self.tracker.remove_lost_segment((200, 600))
diff --git a/tests/cfdp/test_request.py b/tests/cfdp/test_request.py
deleted file mode 100644
index 12ca3a1f..00000000
--- a/tests/cfdp/test_request.py
+++ /dev/null
@@ -1,74 +0,0 @@
-from pathlib import Path
-from unittest import TestCase
-
-from spacepackets.cfdp import CfdpLv, TransactionId
-from spacepackets.cfdp.tlv import (
- OriginatingTransactionId,
- ProxyMessageType,
- ProxyPutRequest,
- ProxyPutRequestParams,
-)
-from spacepackets.util import ByteFieldU16
-
-from tmtccmd.cfdp import PutRequest
-
-
-class TestRequest(TestCase):
- def setUp(self):
- pass
-
- def test_printout_0(self):
- put_req = PutRequest(
- destination_id=ByteFieldU16(5),
- source_file=None,
- dest_file=None,
- trans_mode=None,
- closure_requested=None,
- )
- print_str = str(put_req)
- self.assertEqual(print_str, "Metadata Only Put Request with Destination ID 5")
-
- def test_printout_1(self):
- put_req = PutRequest(
- destination_id=ByteFieldU16(5),
- source_file=Path("/tmp/test.txt"),
- dest_file=Path("/tmp/test2.txt"),
- trans_mode=None,
- closure_requested=None,
- )
- print_str = str(put_req)
- self.assertTrue("Destination ID 5" in print_str)
- self.assertTrue(str(Path("/tmp/test.txt")) in print_str)
- self.assertTrue(str(Path("/tmp/test2.txt")) in print_str)
- self.assertTrue("Transmission Mode from MIB" in print_str)
- self.assertTrue("Closure Requested from MIB" in print_str)
-
- def test_printout_2(self):
- proxy_put = ProxyPutRequest(
- ProxyPutRequestParams(
- dest_entity_id=ByteFieldU16(2),
- source_file_name=CfdpLv.from_str("/tmp/test.txt"),
- dest_file_name=CfdpLv.from_str("/tmp/test2.txt"),
- )
- ).to_generic_msg_to_user_tlv()
- orig_id = TransactionId(
- source_entity_id=ByteFieldU16(1), transaction_seq_num=ByteFieldU16(5)
- )
- orig_id_msg = OriginatingTransactionId(orig_id).to_generic_msg_to_user_tlv()
- put_req = PutRequest(
- destination_id=ByteFieldU16(5),
- source_file=None,
- dest_file=None,
- trans_mode=None,
- closure_requested=None,
- msgs_to_user=[proxy_put, orig_id_msg],
- )
- print_str = str(put_req)
- print(print_str)
- self.assertTrue("Metadata Only Put Request with Destination ID 5" in print_str)
- self.assertTrue(
- f"Message to User 0: Proxy Operation {ProxyMessageType.PUT_REQUEST!r}"
- )
- self.assertTrue("/tmp/test.txt" in print_str)
- self.assertTrue("/tmp/test2.txt" in print_str)
- self.assertTrue("Message to User 1: Originating Transaction ID" in print_str)
diff --git a/tests/cfdp/test_src_handler.py b/tests/cfdp/test_src_handler.py
deleted file mode 100644
index 0f6ae292..00000000
--- a/tests/cfdp/test_src_handler.py
+++ /dev/null
@@ -1,400 +0,0 @@
-import copy
-from pyfakefs.fake_filesystem_unittest import TestCase
-from dataclasses import dataclass
-import tempfile
-from pathlib import Path
-from typing import Optional, Tuple, cast
-from unittest.mock import MagicMock
-
-from crcmod.predefined import PredefinedCrc
-
-from spacepackets.cfdp import (
- FinishedParams,
- PduHolder,
- PduType,
- TransmissionMode,
- NULL_CHECKSUM_U32,
- ConditionCode,
- ChecksumType,
- TransactionId,
-)
-from spacepackets.cfdp.pdu import DirectiveType, EofPdu, FileDataPdu, MetadataPdu
-from spacepackets.util import ByteFieldU16, ByteFieldU32
-from tmtccmd.cfdp import (
- IndicationCfg,
- LocalEntityCfg,
- RemoteEntityCfg,
- CfdpState,
- RemoteEntityCfgTable,
-)
-from tmtccmd.cfdp.handler import SourceHandler, FsmResult
-from tmtccmd.cfdp.exceptions import UnretrievedPdusToBeSent
-from tmtccmd.cfdp.handler.source import TransactionStep
-from tmtccmd.cfdp.user import TransactionParams, TransactionFinishedParams
-from tmtccmd.cfdp.request import PutRequest
-from tmtccmd.util import SeqCountProvider
-from .cfdp_fault_handler_mock import FaultHandler
-from .cfdp_user_mock import CfdpUser
-from .common import CheckTimerProviderForTest
-
-
-@dataclass
-class TransactionStartParams:
- id: TransactionId
- metadata_pdu: MetadataPdu
- file_size: Optional[int]
- crc_32: Optional[bytes]
-
-
-class TestCfdpSourceHandler(TestCase):
-
- """It should be noted that this only verifies the correct generation of PDUs. There is
- no reception handler in play here which would be responsible for generating the files
- from these PDUs
- """
-
- def common_setup(
- self, closure_requested: bool, default_transmission_mode: TransmissionMode
- ):
- self.setUpPyfakefs()
- self.indication_cfg = IndicationCfg(True, True, True, True, True, True)
- self.fault_handler = FaultHandler()
- self.fault_handler.notice_of_cancellation_cb = MagicMock()
- self.fault_handler.notice_of_suspension_cb = MagicMock()
- self.fault_handler.abandoned_cb = MagicMock()
- self.fault_handler.ignore_cb = MagicMock()
- print(self.fault_handler.notice_of_cancellation_cb)
- self.local_cfg = LocalEntityCfg(
- ByteFieldU16(1), self.indication_cfg, self.fault_handler
- )
- self.cfdp_user = CfdpUser()
- self.cfdp_user.eof_sent_indication = MagicMock()
- self.cfdp_user.transaction_indication = MagicMock()
- self.cfdp_user.transaction_finished_indication = MagicMock()
- self.seq_num_provider = SeqCountProvider(bit_width=8)
- self.expected_seq_num = 0
- self.expected_mode = default_transmission_mode
- self.source_id = ByteFieldU16(1)
- self.dest_id = ByteFieldU16(2)
- self.alternative_dest_id = ByteFieldU16(3)
- self.file_segment_len = 64
- self.max_packet_len = 256
- self.positive_ack_intvl_seconds = 0.02
- self.default_remote_cfg = RemoteEntityCfg(
- entity_id=self.dest_id,
- max_packet_len=self.max_packet_len,
- max_file_segment_len=self.file_segment_len,
- closure_requested=closure_requested,
- crc_on_transmission=False,
- default_transmission_mode=default_transmission_mode,
- positive_ack_timer_interval_seconds=self.positive_ack_intvl_seconds,
- positive_ack_timer_expiration_limit=2,
- crc_type=ChecksumType.CRC_32,
- check_limit=2,
- )
- self.alternative_remote_cfg = copy.copy(self.default_remote_cfg)
- self.alternative_remote_cfg.entity_id = self.alternative_dest_id
- self.remote_cfg_table = RemoteEntityCfgTable()
- self.remote_cfg_table.add_config(self.default_remote_cfg)
- self.remote_cfg_table.add_config(self.alternative_remote_cfg)
- # Create an empty file and send it via CFDP
- self.source_handler = SourceHandler(
- cfg=self.local_cfg,
- user=self.cfdp_user,
- remote_cfg_table=self.remote_cfg_table,
- seq_num_provider=self.seq_num_provider,
- check_timer_provider=CheckTimerProviderForTest(),
- )
-
- def _common_empty_file_test(
- self, transmission_mode: Optional[TransmissionMode]
- ) -> Tuple[TransactionId, MetadataPdu, EofPdu]:
- source_path = Path(f"{tempfile.gettempdir()}/hello.txt")
- dest_path = Path(f"{tempfile.gettempdir()}/hello_copy.txt")
- self._generate_file(source_path, bytes())
- self.seq_num_provider.get_and_increment = MagicMock(
- return_value=self.expected_seq_num
- )
- put_req = PutRequest(
- destination_id=self.dest_id,
- source_file=source_path,
- dest_file=dest_path,
- # Let the transmission mode be auto-determined by the remote MIB
- trans_mode=transmission_mode,
- closure_requested=None,
- )
- metadata_pdu, transaction_id = self._start_source_transaction(put_req)
- eof_pdu = self._handle_eof_pdu(transaction_id, NULL_CHECKSUM_U32, 0)
- return transaction_id, metadata_pdu, eof_pdu
-
- def _handle_eof_pdu(
- self,
- id: TransactionId,
- expected_checksum: bytes,
- expected_file_size: int,
- ) -> EofPdu:
- fsm_res = self.source_handler.state_machine()
- self._state_checker(fsm_res, 1, CfdpState.BUSY, TransactionStep.SENDING_EOF)
- self.assertEqual(self.source_handler.transaction_seq_num, id.seq_num)
- next_packet = self.source_handler.get_next_packet()
- self.assertIsNotNone(next_packet)
- assert next_packet is not None
- self.assertEqual(next_packet.pdu_type, PduType.FILE_DIRECTIVE)
- self.assertEqual(next_packet.pdu_directive_type, DirectiveType.EOF_PDU)
- eof_pdu = next_packet.to_eof_pdu()
- self.assertEqual(eof_pdu.transaction_seq_num, id.seq_num)
- # For an empty file, checksum verification does not really make sense, so we expect
- # a null checksum here.
- self.assertEqual(eof_pdu.file_checksum, expected_checksum)
- self.assertEqual(eof_pdu.file_size, expected_file_size)
- self.assertEqual(eof_pdu.condition_code, ConditionCode.NO_ERROR)
- self.assertEqual(eof_pdu.fault_location, None)
- self._verify_eof_indication(id)
- if self.expected_mode == TransmissionMode.ACKNOWLEDGED:
- self._state_checker(
- None,
- 0,
- CfdpState.BUSY,
- TransactionStep.WAITING_FOR_EOF_ACK,
- )
- return eof_pdu
-
- def _common_small_file_test(
- self,
- transmission_mode: Optional[TransmissionMode],
- closure_requested: bool,
- file_content: bytes,
- ) -> Tuple[TransactionId, MetadataPdu, FileDataPdu, EofPdu]:
- source_path = Path(f"{tempfile.gettempdir()}/hello.txt")
- dest_path = Path(f"{tempfile.gettempdir()}/hello_copy.txt")
- self.source_id = ByteFieldU32(1)
- self.dest_id = ByteFieldU32(2)
- self.seq_num_provider.get_and_increment = MagicMock(
- return_value=self.expected_seq_num
- )
- self.source_handler.entity_id = self.source_id
- crc32 = self._gen_crc32(file_content)
- self._generate_file(source_path, file_content)
- put_req = PutRequest(
- destination_id=self.dest_id,
- source_file=source_path,
- dest_file=dest_path,
- # Let the transmission mode be auto-determined by the remote MIB
- trans_mode=transmission_mode,
- closure_requested=closure_requested,
- )
- file_size = source_path.stat().st_size
- metadata_pdu, transaction_id = self._start_source_transaction(put_req)
- self.assertEqual(transaction_id.source_id, self.source_handler.entity_id)
- self.assertEqual(transaction_id.seq_num.value, self.expected_seq_num)
- self.assertEqual(
- self.source_handler.transaction_seq_num.value, self.expected_seq_num
- )
- fsm_res = self.source_handler.state_machine()
- with self.assertRaises(UnretrievedPdusToBeSent):
- self.source_handler.state_machine()
- next_packet = self.source_handler.get_next_packet()
- assert next_packet is not None
- file_data_pdu = self._check_fsm_and_contained_file_data(fsm_res, next_packet)
- eof_pdu = self._handle_eof_pdu(transaction_id, crc32, file_size)
- return transaction_id, metadata_pdu, file_data_pdu, eof_pdu
-
- def _gen_crc32(self, file_content: bytes) -> bytes:
- crc32 = PredefinedCrc("crc32")
- crc32.update(file_content)
- return crc32.digest()
-
- def _generate_file(self, path: Path, file_content: bytes):
- with open(path, "wb") as of:
- data = file_content
- of.write(data)
-
- def _generate_dummy_put_req(self) -> PutRequest:
- return self._generate_generic_put_req(
- Path("dummy-source.txt"), Path("dummy-dest.txt")
- )
-
- def _generate_dest_dummy_put_req(self, source_path: Path) -> PutRequest:
- return self._generate_generic_put_req(source_path, Path("dummy-dest.txt"))
-
- def _generate_generic_put_req(
- self,
- source_path: Path,
- dest_path: Path,
- ) -> PutRequest:
- return PutRequest(
- destination_id=self.dest_id,
- source_file=source_path,
- dest_file=dest_path,
- # Let the transmission mode be auto-determined by the remote MIB
- trans_mode=None,
- closure_requested=False,
- )
-
- def _transaction_with_file_data_wrapper(
- self,
- put_req: PutRequest,
- data: Optional[bytes],
- originating_transaction_id: Optional[TransactionId] = None,
- ) -> TransactionStartParams:
- file_size = None
- crc32 = None
- if data is not None:
- crc32 = self._gen_crc32(data)
- self._generate_file(put_req.source_file, data)
- file_size = put_req.source_file.stat().st_size
- self.local_cfg.local_entity_id = self.source_id
- metadata_pdu, transaction_id = self._start_source_transaction(
- put_req, originating_transaction_id
- )
- self.assertEqual(transaction_id.source_id.value, self.source_id.value)
- self.assertEqual(transaction_id.seq_num.value, self.expected_seq_num)
- return TransactionStartParams(transaction_id, metadata_pdu, file_size, crc32)
-
- def _generic_file_segment_handling(
- self, expected_offset: int, expected_data: bytes
- ) -> FileDataPdu:
- fsm_res = self.source_handler.state_machine()
- self.assertEqual(fsm_res.states.state, CfdpState.BUSY)
- self.assertEqual(self.source_handler.transmission_mode, self.expected_mode)
- self.assertEqual(fsm_res.states.step, TransactionStep.SENDING_FILE_DATA)
- next_packet = self.source_handler.get_next_packet()
- assert next_packet is not None
- self.assertFalse(next_packet.is_file_directive)
- fd_pdu = next_packet.to_file_data_pdu()
- self.assertEqual(fd_pdu.file_data, expected_data)
- self.assertEqual(fd_pdu.offset, expected_offset)
- self.assertEqual(fd_pdu.transaction_seq_num.value, self.expected_seq_num)
- self.assertEqual(fd_pdu.transmission_mode, self.expected_mode)
- return fd_pdu
-
- def _check_fsm_and_contained_file_data(
- self, fsm_res: FsmResult, pdu_holder: PduHolder
- ) -> FileDataPdu:
- self._state_checker(
- fsm_res,
- 0,
- CfdpState.BUSY,
- TransactionStep.SENDING_FILE_DATA,
- )
- self.assertFalse(pdu_holder.is_file_directive)
- return pdu_holder.to_file_data_pdu()
-
- def _start_source_transaction(
- self,
- put_request: PutRequest,
- expected_originating_id: Optional[TransactionId] = None,
- ) -> Tuple[MetadataPdu, TransactionId]:
- self.source_handler.put_request(put_request)
- fsm_res = self.source_handler.state_machine()
- self._state_checker(
- fsm_res,
- 1,
- CfdpState.BUSY,
- TransactionStep.SENDING_METADATA,
- )
- transaction_id = self.source_handler.transaction_id
- assert transaction_id is not None
- self._verify_transaction_indication(expected_originating_id)
- next_packet = self.source_handler.get_next_packet()
- assert next_packet is not None
- self.assertEqual(next_packet.pdu_type, PduType.FILE_DIRECTIVE)
- self.assertEqual(next_packet.pdu_directive_type, DirectiveType.METADATA_PDU)
- metadata_pdu = next_packet.to_metadata_pdu()
- if put_request.closure_requested is not None:
- self.assertEqual(
- metadata_pdu.params.closure_requested, put_request.closure_requested
- )
- self.assertEqual(metadata_pdu.checksum_type, ChecksumType.CRC_32)
- source_file_as_posix = None
- if put_request.source_file is not None:
- source_file_as_posix = put_request.source_file.as_posix()
- self.assertEqual(metadata_pdu.source_file_name, source_file_as_posix)
- dest_file_as_posix = None
- if put_request.dest_file is not None:
- dest_file_as_posix = put_request.dest_file.as_posix()
- self.assertEqual(metadata_pdu.dest_file_name, dest_file_as_posix)
- self.assertEqual(metadata_pdu.dest_entity_id.value, self.dest_id.value)
- return metadata_pdu, transaction_id
-
- def _verify_transaction_indication(
- self, expected_originating_id: Optional[TransactionId]
- ):
- self.cfdp_user.transaction_indication.assert_called_once()
- self.assertEqual(self.cfdp_user.transaction_indication.call_count, 1)
- transaction_params = cast(
- TransactionParams,
- self.cfdp_user.transaction_indication.call_args.args[0],
- )
- self.assertEqual(
- transaction_params.transaction_id, self.source_handler.transaction_id
- )
- self.assertEqual(
- transaction_params.originating_transaction_id, expected_originating_id
- )
-
- def _verify_eof_indication(self, expected_transaction_id: TransactionId):
- self.source_handler.state_machine()
- self.cfdp_user.eof_sent_indication.assert_called_once()
- self.assertEqual(self.cfdp_user.eof_sent_indication.call_count, 1)
- call_args = self.cfdp_user.eof_sent_indication.call_args[0]
- self.assertEqual(len(call_args), 1)
- self.assertEqual(call_args[0], expected_transaction_id)
-
- def _state_checker(
- self,
- fsm_res: Optional[FsmResult],
- num_packets_ready: int,
- expected_state: CfdpState,
- expected_step: TransactionStep,
- ):
- if fsm_res is not None:
- self.assertEqual(fsm_res.states.state, expected_state)
- self.assertEqual(fsm_res.states.step, expected_step)
- if num_packets_ready > 0:
- if fsm_res.states.num_packets_ready != num_packets_ready:
- self.assertEqual(
- fsm_res.states.num_packets_ready, num_packets_ready
- )
- elif num_packets_ready == 0 and fsm_res.states.num_packets_ready > 0:
- packets = []
- while True:
- pdu_holder = self.source_handler.get_next_packet()
- if pdu_holder is None:
- break
- else:
- packets.append(pdu_holder.pdu)
- raise AssertionError(f"Expected no packets, found: {packets}")
- if num_packets_ready > 0:
- self.assertTrue(self.source_handler.packets_ready)
- if expected_state != CfdpState.IDLE:
- self.assertEqual(self.source_handler.transmission_mode, self.expected_mode)
- self.assertEqual(self.source_handler.states.state, expected_state)
- self.assertEqual(self.source_handler.states.step, expected_step)
- self.assertEqual(
- self.source_handler.states.num_packets_ready, num_packets_ready
- )
- self.assertEqual(self.source_handler.state, expected_state)
- self.assertEqual(self.source_handler.step, expected_step)
- self.assertEqual(self.source_handler.num_packets_ready, num_packets_ready)
-
- def _verify_transaction_finished_indication(
- self, expected_id: TransactionId, expected_finish_params: FinishedParams
- ):
- self.cfdp_user.transaction_finished_indication.assert_called_once()
- self.assertEqual(self.cfdp_user.transaction_finished_indication.call_count, 1)
- transaction_finished_params = cast(
- TransactionFinishedParams,
- self.cfdp_user.transaction_finished_indication.call_args.args[0],
- )
- self.assertEqual(transaction_finished_params.transaction_id, expected_id)
- self.assertEqual(
- transaction_finished_params.finished_params, expected_finish_params
- )
-
- def _generate_test_file(self) -> Path:
- source_path = Path(f"{tempfile.gettempdir()}/hello.txt")
- return source_path
-
- def tearDown(self) -> None:
- pass
diff --git a/tests/cfdp/test_src_handler_acked.py b/tests/cfdp/test_src_handler_acked.py
deleted file mode 100644
index f33a3639..00000000
--- a/tests/cfdp/test_src_handler_acked.py
+++ /dev/null
@@ -1,330 +0,0 @@
-from pathlib import Path
-import sys
-import os
-import random
-import time
-import tempfile
-from unittest.mock import MagicMock
-from spacepackets.cfdp import (
- NULL_CHECKSUM_U32,
- ConditionCode,
- Direction,
- DirectiveType,
- PduConfig,
- PduType,
- TransactionId,
- TransmissionMode,
- FinishedParams,
-)
-from spacepackets.cfdp.pdu import (
- AckPdu,
- DeliveryCode,
- EofPdu,
- FileStatus,
- FinishedPdu,
- NakPdu,
- TransactionStatus,
-)
-
-from tmtccmd.cfdp.defs import CfdpState
-from tmtccmd.cfdp.handler.source import TransactionStep
-from .test_src_handler import TestCfdpSourceHandler
-
-
-class TestSourceHandlerAcked(TestCfdpSourceHandler):
- def setUp(self) -> None:
- self.common_setup(True, TransmissionMode.ACKNOWLEDGED)
-
- def test_empty_file_transfer(self):
- transaction_id, _, eof_pdu = self._common_empty_file_test(None)
- self._generic_acked_transfer_completion_full_success(transaction_id, eof_pdu)
-
- def test_small_file_transfer(self):
- transaction_id, _, _, eof_pdu = self._common_small_file_test(
- TransmissionMode.ACKNOWLEDGED,
- True,
- "Hello World!".encode(),
- )
- self._generic_acked_transfer_completion_full_success(transaction_id, eof_pdu)
-
- def test_missing_metadata_pdu_retransmission(self):
- transaction_id, first_metadata_pdu, eof_pdu = self._common_empty_file_test(None)
- # Generate appropriate NAK PDU and insert it.
- nak_missing_metadata = NakPdu(eof_pdu.pdu_header.pdu_conf, 0, 0, [(0, 0)])
- self.source_handler.insert_packet(nak_missing_metadata)
- self.source_handler.state_machine()
- self._state_checker(
- None,
- True,
- CfdpState.BUSY,
- TransactionStep.RETRANSMITTING,
- )
- next_pdu = self.source_handler.get_next_packet()
- assert next_pdu is not None
- self.assertEqual(next_pdu.pdu_type, PduType.FILE_DIRECTIVE)
- self.assertEqual(next_pdu.pdu_directive_type, DirectiveType.METADATA_PDU)
- metadata_pdu = next_pdu.to_metadata_pdu()
- self.assertEqual(metadata_pdu, first_metadata_pdu)
- self.source_handler.state_machine()
- self._generic_acked_transfer_completion_full_success(transaction_id, eof_pdu)
-
- def test_missing_filedata_pdu_retransmission(self):
- file_content = "Hello World!".encode()
- transaction_id, _, first_fd_pdu, eof_pdu = self._common_small_file_test(
- TransmissionMode.ACKNOWLEDGED, True, file_content
- )
- end_of_scope = len(file_content)
- # Generate appropriate NAK PDU and insert it.
- nak_missing_metadata = NakPdu(
- eof_pdu.pdu_header.pdu_conf, 0, end_of_scope, [(0, end_of_scope)]
- )
- self.source_handler.insert_packet(nak_missing_metadata)
- self.source_handler.state_machine()
- self._state_checker(
- None,
- 1,
- CfdpState.BUSY,
- TransactionStep.RETRANSMITTING,
- )
- next_pdu = self.source_handler.get_next_packet()
- assert next_pdu is not None
- self.assertEqual(next_pdu.pdu_type, PduType.FILE_DATA)
- fd_pdu = next_pdu.to_file_data_pdu()
- self.assertEqual(fd_pdu, first_fd_pdu)
- self.source_handler.state_machine()
- self._generic_acked_transfer_completion_full_success(transaction_id, eof_pdu)
-
- def test_positive_ack_procedure(self):
- # 1. Send EOF PDU.
- # 2. Verify EOF PDU is sent again after ACK limit is reached once.
- _, _, initial_eof_pdu = self._common_empty_file_test(None)
- self.assertEqual(self.source_handler.positive_ack_counter, 0)
- time.sleep(self.positive_ack_intvl_seconds * 1.2)
- self._verify_eof_pdu_for_positive_ack(initial_eof_pdu, 1)
-
- def test_ack_limit_reached(self):
- # This tests fully checks the case where the ACK limit is reached and a corresponding
- # fault is declared.
- transaction_id, _, initial_eof_pdu = self._common_empty_file_test(None)
- self.assertEqual(self.source_handler.positive_ack_counter, 0)
- # 100 ms timeout, sleep of 150 ms should trigger EOF re-send.
- time.sleep(self.positive_ack_intvl_seconds * 1.2)
- self._verify_eof_pdu_for_positive_ack(initial_eof_pdu, 1)
- time.sleep(self.positive_ack_intvl_seconds * 1.2)
- self.source_handler.state_machine()
- # That's odd but expected. The ACK limit reached fault will trigger a notice
- # of cancellation by default, which causes it to send an EOF PDU with the appropriate
- # condition code. There is little chance that this PDU will arrive after all the
- # others did not arrive in a real use case, but this is standard conformant behaviour.
- # If the the positive ACK procedure fails for this EOF PDU, the standard behaviour
- # is abandonment of the transaction, irrespective of any fault handler configuration,
- # according to section 4.11.2.2.3 of the standard.
- self._state_checker(
- None,
- 1,
- CfdpState.BUSY,
- TransactionStep.SENDING_EOF,
- )
- cancelation_cb_mock: MagicMock = self.fault_handler.notice_of_cancellation_cb # type: ignore
- cancelation_cb_mock.assert_called_once()
- cancelation_cb_mock.assert_called_with(
- transaction_id, ConditionCode.POSITIVE_ACK_LIMIT_REACHED, 0
- )
-
- next_pdu = self.source_handler.get_next_packet()
- assert next_pdu is not None
- self.assertEqual(next_pdu.pdu_type, PduType.FILE_DIRECTIVE)
- self.assertEqual(next_pdu.pdu_directive_type, DirectiveType.EOF_PDU)
- eof_pdu_for_cancellation = next_pdu.to_eof_pdu()
- self.assertEqual(
- eof_pdu_for_cancellation.condition_code,
- ConditionCode.POSITIVE_ACK_LIMIT_REACHED,
- )
- self.assertEqual(eof_pdu_for_cancellation.file_checksum, NULL_CHECKSUM_U32)
- self.assertEqual(eof_pdu_for_cancellation.file_size, 0)
- # Calling the state machine again confirms we sent or handled the EOF packet,
- # and only then will the positive ACK counter be reset.
- self.source_handler.state_machine()
- self._state_checker(
- None, 0, CfdpState.BUSY, TransactionStep.WAITING_FOR_EOF_ACK
- )
- self.assertEqual(self.source_handler.positive_ack_counter, 0)
- time.sleep(self.positive_ack_intvl_seconds * 1.2)
- self._verify_eof_pdu_for_positive_ack(eof_pdu_for_cancellation, 1)
- time.sleep(self.positive_ack_intvl_seconds * 1.2)
- self.source_handler.state_machine()
- self.expected_cfdp_state = CfdpState.IDLE
- # Transaction was abandoned.
- self._state_checker(
- None,
- 0,
- CfdpState.IDLE,
- TransactionStep.IDLE,
- )
- abandoned_cb_mock: MagicMock = self.fault_handler.abandoned_cb # type: ignore
- abandoned_cb_mock.assert_called_once()
- abandoned_cb_mock.assert_called_with(
- transaction_id, ConditionCode.POSITIVE_ACK_LIMIT_REACHED, 0
- )
-
- def test_ack_procedure_success(self):
- # 1. Send EOF PDU.
- # 2. Verify EOF PDU is sent again after ACK limit is reached once.
- self.assertEqual(self.source_handler.positive_ack_counter, 0)
- transaction_id, _, initial_eof_pdu = self._common_empty_file_test(None)
- time.sleep(self.positive_ack_intvl_seconds * 1.2)
- self._verify_eof_pdu_for_positive_ack(initial_eof_pdu, 1)
- self._generic_acked_transfer_completion_full_success(
- transaction_id, initial_eof_pdu
- )
-
- def test_large_missing_chunk_retransmission(self):
- # This tests generates three file data PDUs
- if sys.version_info >= (3, 9):
- rand_data = random.randbytes(self.file_segment_len * 3)
- else:
- rand_data = os.urandom(self.file_segment_len * 3)
- crc32 = self._gen_crc32(rand_data)
- source_path = Path(f"{tempfile.gettempdir()}/rand-three-segs.bin")
- self._generate_file(source_path, rand_data)
- dest_path = Path(f"{tempfile.gettempdir()}/rand-three-segs-copy.bin")
- transaction_params = self._transaction_with_file_data_wrapper(
- self._generate_generic_put_req(source_path, dest_path), rand_data
- )
- current_idx = 0
- fd_pdu_list = []
- while current_idx < len(rand_data):
- fd_pdu_list.append(
- self._generic_file_segment_handling(
- current_idx,
- rand_data[current_idx : current_idx + self.file_segment_len],
- )
- )
- current_idx += self.file_segment_len
- eof_pdu = self._handle_eof_pdu(transaction_params.id, crc32, len(rand_data))
- all_missing_filedata = NakPdu(
- pdu_conf=eof_pdu.pdu_header.pdu_conf,
- start_of_scope=0,
- end_of_scope=len(rand_data),
- segment_requests=[(0, len(rand_data))],
- )
- self.source_handler.insert_packet(all_missing_filedata)
- fsm_res = self.source_handler.state_machine()
- self._state_checker(fsm_res, 3, CfdpState.BUSY, TransactionStep.RETRANSMITTING)
- # All file data PDUs should be re-sent now.
- for i in range(3):
- next_pdu = self.source_handler.get_next_packet()
- assert next_pdu is not None
- self.assertEqual(fd_pdu_list[i], next_pdu.pdu)
- next_pdu = self.source_handler.get_next_packet()
- assert next_pdu is None
- self.source_handler.state_machine()
- self._generic_acked_transfer_completion_full_success(
- transaction_params.id, eof_pdu
- )
-
- def _generic_acked_transfer_completion_full_success(
- self, transaction_id: TransactionId, eof_pdu: EofPdu
- ):
- self._generic_acked_transfer_completion(
- transaction_id,
- eof_pdu,
- FinishedParams(
- condition_code=ConditionCode.NO_ERROR,
- file_status=FileStatus.FILE_RETAINED,
- delivery_code=DeliveryCode.DATA_COMPLETE,
- ),
- )
-
- def _generic_acked_transfer_completion(
- self,
- transaction_id: TransactionId,
- eof_pdu: EofPdu,
- expected_finished_params: FinishedParams,
- ):
- self._state_checker(
- None,
- 0,
- CfdpState.BUSY,
- TransactionStep.WAITING_FOR_EOF_ACK,
- )
- pdu_conf = eof_pdu.pdu_header.pdu_conf
- self._insert_eof_ack_packet(eof_pdu)
- self._insert_finished_pdu(pdu_conf)
- self._acknowledge_finished_pdu(pdu_conf)
- self.source_handler.state_machine()
- self._verify_transaction_finished_indication(
- transaction_id, expected_finished_params
- )
- self.expected_cfdp_state = CfdpState.IDLE
- self._state_checker(
- None,
- False,
- CfdpState.IDLE,
- TransactionStep.IDLE,
- )
-
- def _insert_finished_pdu(self, pdu_conf: PduConfig):
- finished_pdu = FinishedPdu(
- pdu_conf,
- FinishedParams(
- delivery_code=DeliveryCode.DATA_COMPLETE,
- file_status=FileStatus.FILE_RETAINED,
- condition_code=ConditionCode.NO_ERROR,
- ),
- )
- self.source_handler.insert_packet(finished_pdu)
- self.source_handler.state_machine()
- self._state_checker(
- None,
- True,
- CfdpState.BUSY,
- TransactionStep.SENDING_ACK_OF_FINISHED,
- )
-
- def _verify_eof_pdu_for_positive_ack(
- self, expected_eof: EofPdu, expected_ack_counter: int
- ):
- self.source_handler.state_machine()
- self.assertEqual(self.source_handler.packets_ready, True)
- self.assertEqual(self.source_handler.num_packets_ready, 1)
- self.assertEqual(self.source_handler.positive_ack_counter, expected_ack_counter)
- next_pdu = self.source_handler.get_next_packet()
- assert next_pdu is not None
- self.assertEqual(next_pdu.pdu_type, PduType.FILE_DIRECTIVE)
- self.assertEqual(next_pdu.pdu_directive_type, DirectiveType.EOF_PDU)
- eof_pdu = next_pdu.to_eof_pdu()
- self.assertEqual(eof_pdu, expected_eof)
-
- def _acknowledge_finished_pdu(self, pdu_conf: PduConfig):
- next_pdu = self.source_handler.get_next_packet()
- assert next_pdu is not None
- self.assertEqual(next_pdu.pdu_type, PduType.FILE_DIRECTIVE)
- self.assertEqual(next_pdu.pdu_directive_type, DirectiveType.ACK_PDU)
- ack_pdu = next_pdu.to_ack_pdu()
- self.assertEqual(ack_pdu.direction, Direction.TOWARDS_RECEIVER)
- self.assertEqual(
- ack_pdu.directive_code_of_acked_pdu, DirectiveType.FINISHED_PDU
- )
- self.assertEqual(ack_pdu.transaction_status, TransactionStatus.ACTIVE)
- # Set this correctly for test explicitely. Every other field should be the same.
- pdu_conf.direction = Direction.TOWARDS_RECEIVER
- self.assertEqual(ack_pdu.pdu_header.pdu_conf, pdu_conf)
-
- def _insert_eof_ack_packet(self, eof_pdu: EofPdu):
- pdu_conf = eof_pdu.pdu_header.pdu_conf
- ack_pdu = AckPdu(
- pdu_conf,
- DirectiveType.EOF_PDU,
- ConditionCode.NO_ERROR,
- TransactionStatus.ACTIVE,
- )
- self.assertEqual(ack_pdu.direction, Direction.TOWARDS_SENDER)
- self.source_handler.insert_packet(ack_pdu)
- self.source_handler.state_machine()
- self._state_checker(
- None,
- False,
- CfdpState.BUSY,
- TransactionStep.WAITING_FOR_FINISHED,
- )
diff --git a/tests/cfdp/test_src_handler_nak_closure.py b/tests/cfdp/test_src_handler_nak_closure.py
deleted file mode 100644
index 83b1e154..00000000
--- a/tests/cfdp/test_src_handler_nak_closure.py
+++ /dev/null
@@ -1,187 +0,0 @@
-import os
-from pathlib import Path
-import tempfile
-import random
-import sys
-from unittest.mock import MagicMock
-
-from spacepackets.cfdp import (
- ConditionCode,
- Direction,
- DirectiveType,
- TransmissionMode,
- PduConfig,
-)
-from spacepackets.cfdp.pdu import FinishedPdu, DeliveryCode, FileStatus
-from spacepackets.cfdp.pdu.finished import FinishedParams
-from spacepackets.util import (
- ByteFieldU8,
- ByteFieldU16,
- ByteFieldEmpty,
-)
-from tmtccmd.cfdp.defs import CfdpState
-from tmtccmd.cfdp import PutRequest
-from tmtccmd.cfdp.exceptions import (
- InvalidPduDirection,
- InvalidSourceId,
- InvalidDestinationId,
-)
-from tmtccmd.cfdp.handler.source import TransactionStep
-from .test_src_handler import TestCfdpSourceHandler
-
-
-class TestCfdpSourceHandlerWithClosure(TestCfdpSourceHandler):
- def setUp(self) -> None:
- self.common_setup(True, TransmissionMode.UNACKNOWLEDGED)
- self.seq_num_provider.get_and_increment = MagicMock(return_value=2)
-
- def test_empty_file_pdu_generation_nacked_by_remote_cfg(self):
- transaction_id, metadata_pdu, _ = self._common_empty_file_test(None)
- self._pass_simple_finish_pdu_to_source_handler(metadata_pdu.pdu_header.pdu_conf)
- # Transaction should be finished
- fsm_res = self.source_handler.state_machine()
- self._verify_transaction_finished_indication(
- transaction_id,
- FinishedParams(
- condition_code=ConditionCode.NO_ERROR,
- file_status=FileStatus.FILE_RETAINED,
- delivery_code=DeliveryCode.DATA_COMPLETE,
- ),
- )
- self._state_checker(fsm_res, False, CfdpState.IDLE, TransactionStep.IDLE)
-
- def test_empty_file_pdu_generation_nacked_explicitely(self):
- self.default_remote_cfg.default_transmission_mode = (
- TransmissionMode.ACKNOWLEDGED
- )
- transaction_id, metadata_pdu, _ = self._common_empty_file_test(
- TransmissionMode.UNACKNOWLEDGED
- )
- self._pass_simple_finish_pdu_to_source_handler(metadata_pdu.pdu_header.pdu_conf)
- # Transaction should be finished
- fsm_res = self.source_handler.state_machine()
- self._verify_transaction_finished_indication(
- transaction_id,
- FinishedParams(
- condition_code=ConditionCode.NO_ERROR,
- file_status=FileStatus.FILE_RETAINED,
- delivery_code=DeliveryCode.DATA_COMPLETE,
- ),
- )
- self.expected_cfdp_state = CfdpState.IDLE
- self._state_checker(fsm_res, False, CfdpState.IDLE, TransactionStep.IDLE)
-
- def test_small_file_pdu_generation(self):
- file_content = "Hello World\n".encode()
- transaction_id, metadata_pdu, _, _ = self._common_small_file_test(
- TransmissionMode.UNACKNOWLEDGED, True, file_content
- )
- self._verify_eof_indication(transaction_id)
- self.source_handler.state_machine()
- self._pass_simple_finish_pdu_to_source_handler(metadata_pdu.pdu_header.pdu_conf)
- # Transaction should be finished
- fsm_res = self.source_handler.state_machine()
- self.expected_cfdp_state = CfdpState.IDLE
- self._state_checker(fsm_res, False, CfdpState.IDLE, TransactionStep.IDLE)
-
- def test_invalid_dir_pdu_passed(self):
- self.dest_id = ByteFieldU16(2)
- source_path = Path(f"{tempfile.gettempdir()}/dummy.txt")
- self._generate_file(source_path, bytes())
- metadata_pdu, _ = self._start_source_transaction(
- self._generate_generic_put_req(source_path, Path("dummy.txt"))
- )
- finish_pdu = self._prepare_finish_pdu(metadata_pdu.pdu_header.pdu_conf)
- finish_pdu.pdu_file_directive.pdu_header.direction = Direction.TOWARDS_RECEIVER
- with self.assertRaises(InvalidPduDirection):
- self.source_handler.insert_packet(finish_pdu)
-
- def test_cancelled_transaction(self):
- # This tests generates two file data PDUs
- if sys.version_info >= (3, 9):
- rand_data = random.randbytes(self.file_segment_len * 2)
- else:
- rand_data = os.urandom(self.file_segment_len * 2)
- self.source_id = ByteFieldU8(1)
- self.dest_id = ByteFieldU8(2)
- self._update_seq_num_to_use(3)
- source_path = Path(f"{tempfile.gettempdir()}/two-segments.bin")
- dest_path = Path(f"{tempfile.gettempdir()}/two-segments-copy.bin")
- # The calculated CRC in the EOF (Cancel) PDU will only be calculated for the first segment
- tparams = self._transaction_with_file_data_wrapper(
- self._generate_generic_put_req(source_path, dest_path),
- rand_data[0 : self.file_segment_len],
- )
- self._generic_file_segment_handling(0, rand_data[0 : self.file_segment_len])
- self.assertTrue(self.source_handler.cancel_request(tparams.id))
- self.assertEqual(self.source_handler.step, TransactionStep.SENDING_EOF)
- next_packet = self.source_handler.get_next_packet()
- assert next_packet is not None
- self.assertTrue(next_packet.is_file_directive)
- self.assertEqual(next_packet.pdu_directive_type, DirectiveType.EOF_PDU)
- eof_pdu = next_packet.to_eof_pdu()
- self.assertEqual(tparams.crc_32, eof_pdu.file_checksum)
- self.assertEqual(eof_pdu.file_size, self.file_segment_len)
- self.assertEqual(eof_pdu.file_size, tparams.file_size)
- fsm_res = self.source_handler.state_machine()
- self.expected_cfdp_state = CfdpState.IDLE
- self._state_checker(fsm_res, False, CfdpState.IDLE, TransactionStep.IDLE)
-
- def test_invalid_source_id_pdu_passed(self):
- source_path = Path(f"{tempfile.gettempdir()}/test.txt")
- self._update_seq_num_to_use(2)
- self._generate_file(source_path, bytes())
- put_req = self._generate_dest_dummy_put_req(source_path)
- finish_pdu = self._regular_transaction_start(put_req)
- finish_pdu.pdu_file_directive.pdu_conf.source_entity_id = ByteFieldEmpty()
- with self.assertRaises(InvalidSourceId) as cm:
- self.source_handler.insert_packet(finish_pdu)
- exception = cm.exception
- self.assertEqual(exception.found_src_id, ByteFieldEmpty())
- self.assertEqual(exception.expected_src_id, ByteFieldU16(1))
-
- def test_invalid_dest_id_pdu_passed(self):
- source_path = Path(f"{tempfile.gettempdir()}/test.txt")
- self._update_seq_num_to_use(2)
- self.dest_id = ByteFieldU16(3)
- self._generate_file(source_path, bytes())
- put_req = self._generate_dest_dummy_put_req(source_path)
- finish_pdu = self._regular_transaction_start(put_req)
- finish_pdu.pdu_file_directive.pdu_conf.dest_entity_id = ByteFieldEmpty()
- with self.assertRaises(InvalidDestinationId) as cm:
- self.source_handler.insert_packet(finish_pdu)
- exception = cm.exception
- self.assertEqual(exception.found_dest_id, ByteFieldEmpty())
- self.assertEqual(exception.expected_dest_id, ByteFieldU16(3))
-
- def _update_seq_num_to_use(self, seq_num: int):
- self.expected_seq_num = seq_num
- self.seq_num_provider.get_and_increment = MagicMock(
- return_value=self.expected_seq_num
- )
-
- def _regular_transaction_start(self, put_req: PutRequest) -> FinishedPdu:
- metadata_pdu, _ = self._start_source_transaction(put_req)
- finish_pdu = self._prepare_finish_pdu(metadata_pdu.pdu_header.pdu_conf)
- self.assertEqual(finish_pdu.transaction_seq_num.value, self.expected_seq_num)
- return finish_pdu
-
- def _pass_simple_finish_pdu_to_source_handler(self, base_conf: PduConfig):
- self._state_checker(
- None,
- False,
- CfdpState.BUSY,
- TransactionStep.WAITING_FOR_FINISHED,
- )
- self.source_handler.insert_packet(self._prepare_finish_pdu(base_conf))
-
- def _prepare_finish_pdu(self, base_conf: PduConfig):
- params = FinishedParams(
- delivery_code=DeliveryCode.DATA_COMPLETE,
- condition_code=ConditionCode.NO_ERROR,
- file_status=FileStatus.FILE_RETAINED,
- )
- return FinishedPdu(
- params=params,
- pdu_conf=base_conf,
- )
diff --git a/tests/cfdp/test_src_handler_nak_no_closure.py b/tests/cfdp/test_src_handler_nak_no_closure.py
deleted file mode 100644
index 20b9c59b..00000000
--- a/tests/cfdp/test_src_handler_nak_no_closure.py
+++ /dev/null
@@ -1,260 +0,0 @@
-import os
-import random
-import sys
-import tempfile
-from pathlib import Path
-
-from spacepackets.cfdp import (
- CfdpLv,
- DirectiveType,
- ConditionCode,
- PduType,
- TransmissionMode,
- TransactionId,
-)
-from spacepackets.cfdp.pdu import FileStatus, DeliveryCode
-from spacepackets.cfdp.pdu.finished import FinishedParams
-from spacepackets.cfdp.tlv import (
- ProxyPutRequest,
- ProxyPutRequestParams,
- ProxyPutResponse,
- ProxyPutResponseParams,
- OriginatingTransactionId,
-)
-from spacepackets.util import ByteFieldU16, ByteFieldU8
-from tmtccmd.cfdp.defs import CfdpState
-from tmtccmd.cfdp.handler import FsmResult
-from tmtccmd.cfdp.handler.source import TransactionStep
-from tmtccmd.cfdp.request import PutRequest
-from tmtccmd.cfdp.user import TransactionFinishedParams, TransactionParams
-from .test_src_handler import TestCfdpSourceHandler
-
-
-class TestCfdpSourceHandlerNackedNoClosure(TestCfdpSourceHandler):
- def setUp(self) -> None:
- self.common_setup(False, TransmissionMode.UNACKNOWLEDGED)
-
- def test_empty_file_nacked_by_def_config(self):
- transaction_id, _, _ = self._common_empty_file_test(
- transmission_mode=None,
- )
- fsm_res = self.source_handler.state_machine()
- self.source_handler.state_machine()
- self._verify_transaction_finished_indication(
- transaction_id,
- FinishedParams(
- condition_code=ConditionCode.NO_ERROR,
- file_status=FileStatus.FILE_STATUS_UNREPORTED,
- delivery_code=DeliveryCode.DATA_COMPLETE,
- ),
- )
- self.expected_cfdp_state = CfdpState.IDLE
- self._state_checker(fsm_res, False, CfdpState.IDLE, TransactionStep.IDLE)
-
- def test_empty_file_explicit_nacked(self):
- self.default_remote_cfg.default_transmission_mode = (
- TransmissionMode.ACKNOWLEDGED
- )
- transaction_id, _, _ = self._common_empty_file_test(
- transmission_mode=TransmissionMode.UNACKNOWLEDGED,
- )
- fsm_res = self.source_handler.state_machine()
- self.source_handler.state_machine()
- self._verify_transaction_finished_indication(
- transaction_id,
- FinishedParams(
- condition_code=ConditionCode.NO_ERROR,
- file_status=FileStatus.FILE_STATUS_UNREPORTED,
- delivery_code=DeliveryCode.DATA_COMPLETE,
- ),
- )
- self.expected_cfdp_state = CfdpState.IDLE
- self._state_checker(fsm_res, False, CfdpState.IDLE, TransactionStep.IDLE)
-
- def test_small_file_pdu_generation(self):
- file_content = "Hello World\n".encode()
- transaction_id, _, _, _ = self._common_small_file_test(
- None, False, file_content
- )
- self._verify_eof_indication(transaction_id)
- self._test_transaction_completion()
-
- def test_perfectly_segmented_file_pdu_generation(self):
- # This tests generates two file data PDUs
- if sys.version_info >= (3, 9):
- rand_data = random.randbytes(self.file_segment_len * 2)
- else:
- rand_data = os.urandom(self.file_segment_len * 2)
- self.source_id = ByteFieldU8(1)
- self.dest_id = ByteFieldU8(2)
- source_path = Path(f"{tempfile.gettempdir()}/two-segments.bin")
- dest_path = Path(f"{tempfile.gettempdir()}/two-segments-copy.bin")
- tparams = self._transaction_with_file_data_wrapper(
- self._generate_generic_put_req(source_path, dest_path), rand_data
- )
- self._generic_file_segment_handling(0, rand_data[0 : self.file_segment_len])
- self._generic_file_segment_handling(
- self.file_segment_len, rand_data[self.file_segment_len :]
- )
- fsm_res = self.source_handler.state_machine()
- self._test_eof_file_pdu(fsm_res, tparams.file_size, tparams.crc_32)
- self._test_transaction_completion()
-
- def test_segmented_file_pdu_generation(self):
- # This tests generates two file data PDUs, but the second one does not have a
- # full segment length
- if sys.version_info >= (3, 9):
- rand_data = random.randbytes(round(self.file_segment_len * 1.3))
- else:
- rand_data = os.urandom(round(self.file_segment_len * 1.3))
- self.source_id = ByteFieldU16(2)
- self.dest_id = ByteFieldU16(3)
- self.source_handler.source_id = self.source_id
- source_path = Path(f"{tempfile.gettempdir()}/hello-source.txt")
- dest_path = Path(f"{tempfile.gettempdir()}/hello-dest.txt")
- tparams = self._transaction_with_file_data_wrapper(
- self._generate_generic_put_req(source_path, dest_path), rand_data
- )
- self._generic_file_segment_handling(0, rand_data[0 : self.file_segment_len])
- self._generic_file_segment_handling(
- self.file_segment_len, rand_data[self.file_segment_len :]
- )
- fsm_res = self.source_handler.state_machine()
- self._test_eof_file_pdu(fsm_res, tparams.file_size, tparams.crc_32)
- self._test_transaction_completion()
-
- def test_proxy_get_request(self):
- proxy_op_params = ProxyPutRequestParams(
- self.local_cfg.local_entity_id,
- CfdpLv.from_str(f"{tempfile.gettempdir()}/source.txt"),
- CfdpLv.from_str(f"{tempfile.gettempdir()}/dest.txt"),
- )
- proxy_op = ProxyPutRequest(proxy_op_params)
- generic_tlv = proxy_op.to_generic_msg_to_user_tlv()
- put_req = PutRequest(
- destination_id=self.dest_id,
- source_file=None,
- dest_file=None,
- # Let the transmission mode be auto-determined by the remote MIB
- trans_mode=None,
- closure_requested=None,
- msgs_to_user=[generic_tlv],
- )
- self.source_handler.put_request(put_req)
- fsm_res = self.source_handler.state_machine()
- next_packet = self.source_handler.get_next_packet()
- assert next_packet is not None
- self.assertEqual(next_packet.pdu_type, PduType.FILE_DIRECTIVE)
- self.assertEqual(next_packet.pdu_directive_type, DirectiveType.METADATA_PDU)
- metadata_pdu = next_packet.to_metadata_pdu()
- self.assertIsNotNone(metadata_pdu.options)
- self.assertEqual(len(metadata_pdu.options), 1)
- self.assertEqual(metadata_pdu.options[0], generic_tlv)
- self.assertIsNone(metadata_pdu.source_file_name)
- self.assertIsNone(metadata_pdu.dest_file_name)
- self.assertEqual(fsm_res.states.state, CfdpState.BUSY)
- self.assertEqual(
- self.source_handler.transmission_mode, TransmissionMode.UNACKNOWLEDGED
- )
- self.assertEqual(fsm_res.states.step, TransactionStep.SENDING_METADATA)
- expected_id = TransactionId(
- metadata_pdu.source_entity_id, metadata_pdu.transaction_seq_num
- )
- self.cfdp_user.transaction_indication.assert_called_once_with(
- TransactionParams(expected_id)
- )
- # Now the state machine should be finished.
- fsm_res = self.source_handler.state_machine()
- finished_params = TransactionFinishedParams(
- transaction_id=expected_id,
- finished_params=FinishedParams(
- condition_code=ConditionCode.NO_ERROR,
- file_status=FileStatus.FILE_STATUS_UNREPORTED,
- delivery_code=DeliveryCode.DATA_COMPLETE,
- ),
- )
- self.cfdp_user.transaction_finished_indication.assert_called_once_with(
- finished_params
- )
- self._state_checker(fsm_res, 0, CfdpState.IDLE, TransactionStep.IDLE)
-
- def test_put_req_by_proxy_op(self):
- file_content = "Hello World\n".encode()
- dest_path = Path(f"{tempfile.gettempdir()}/dest.txt")
- originating_id = TransactionId(
- ByteFieldU16(5), ByteFieldU16(self.expected_seq_num)
- )
- originating_id_msg = OriginatingTransactionId(originating_id)
- put_req = PutRequest(
- destination_id=self.dest_id,
- source_file=Path(f"{tempfile.gettempdir()}/source.txt"),
- dest_file=dest_path,
- # Let the transmission mode be auto-determined by the remote MIB.
- trans_mode=None,
- closure_requested=None,
- msgs_to_user=[originating_id_msg.to_generic_msg_to_user_tlv()],
- )
-
- self.source_id = ByteFieldU8(1)
- self.dest_id = ByteFieldU8(2)
- self.source_handler.source_id = self.source_id
- tparams = self._transaction_with_file_data_wrapper(
- put_req, file_content, originating_id
- )
- self._generic_file_segment_handling(0, file_content)
- fsm_res = self.source_handler.state_machine()
- self._test_eof_file_pdu(fsm_res, tparams.file_size, tparams.crc_32)
- self._test_transaction_completion()
-
- def test_proxy_put_response_no_originating_id(self):
- """Proxy put responses should not pass the originating ID to the CFDP user to avoid
- permanent loops of trying to finish a proxy put request."""
- originating_id = TransactionId(
- ByteFieldU16(5), ByteFieldU16(self.expected_seq_num)
- )
- originating_id_msg = OriginatingTransactionId(originating_id)
- put_req = PutRequest(
- destination_id=self.dest_id,
- source_file=None,
- dest_file=None,
- # Let the transmission mode be auto-determined by the remote MIB.
- trans_mode=None,
- closure_requested=None,
- msgs_to_user=[
- ProxyPutResponse(
- ProxyPutResponseParams.from_finished_params(
- FinishedParams(
- DeliveryCode.DATA_COMPLETE,
- ConditionCode.NO_ERROR,
- FileStatus.FILE_RETAINED,
- )
- )
- ).to_generic_msg_to_user_tlv(),
- originating_id_msg.to_generic_msg_to_user_tlv(),
- ],
- )
-
- self.source_id = ByteFieldU8(1)
- self.dest_id = ByteFieldU8(2)
- self.source_handler.source_id = self.source_id
- self._transaction_with_file_data_wrapper(
- put_req, data=None, originating_transaction_id=None
- )
- self.source_handler.state_machine()
- self._test_transaction_completion()
-
- def _test_eof_file_pdu(self, fsm_res: FsmResult, file_size: int, crc32: bytes):
- self._state_checker(fsm_res, 1, CfdpState.BUSY, TransactionStep.SENDING_EOF)
- next_packet = self.source_handler.get_next_packet()
- assert next_packet is not None
- self.assertEqual(next_packet.pdu_type, PduType.FILE_DIRECTIVE)
- self.assertEqual(next_packet.pdu_directive_type, DirectiveType.EOF_PDU)
- eof_pdu = next_packet.to_eof_pdu()
- self.assertEqual(eof_pdu.file_size, file_size)
- self.assertEqual(eof_pdu.file_checksum, crc32)
-
- def _test_transaction_completion(self):
- fsm_res = self.source_handler.state_machine()
- self._state_checker(fsm_res, 0, CfdpState.IDLE, TransactionStep.IDLE)
- self.cfdp_user.transaction_finished_indication.assert_called_once()
- self.assertEqual(self.cfdp_user.transaction_finished_indication.call_count, 1)
diff --git a/tests/config/test_cfdp_conversions.py b/tests/config/test_cfdp_conversions.py
index 4239f630..dc03258b 100644
--- a/tests/config/test_cfdp_conversions.py
+++ b/tests/config/test_cfdp_conversions.py
@@ -4,7 +4,7 @@
from spacepackets.cfdp import CfdpLv
from spacepackets.cfdp.tlv import ProxyMessageType
from spacepackets.util import ByteFieldU8
-from tmtccmd.cfdp.request import PutRequest
+from cfdppy.request import PutRequest
from tmtccmd.config.cfdp import (
cfdp_req_to_put_req_regular,
cfdp_req_to_put_req_get_req,
diff --git a/tests/test_countdown.py b/tests/test_countdown.py
deleted file mode 100644
index 942abc13..00000000
--- a/tests/test_countdown.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import time
-from datetime import timedelta
-from unittest import TestCase
-from tmtccmd.util.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.remaining_time().total_seconds() * 1000 > 40)
- time.sleep(0.05)
- self.assertTrue(test_cd.timed_out())
- self.assertTrue(test_cd.remaining_time() == timedelta())
- test_cd.timeout = timedelta(seconds=0.1)
- self.assertEqual(
- test_cd.timeout.total_seconds(), timedelta(seconds=0.1).total_seconds()
- )
- self.assertEqual(test_cd.timeout_ms, 100)
- 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.remaining_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_printer.py b/tests/test_printer.py
index 258b32fc..d41eb2bd 100644
--- a/tests/test_printer.py
+++ b/tests/test_printer.py
@@ -37,7 +37,7 @@ def test_pus_loggers(self):
subservice=Subservice.TM_START_SUCCESS,
time_provider=CdsShortTimestamp.from_now(),
verif_params=VerificationParams(
- req_id=RequestId(pus_tc.packet_id, pus_tc.packet_seq_ctrl)
+ req_id=RequestId(pus_tc.packet_id, pus_tc.packet_seq_control)
),
)
raw_tmtc_log.log_tm(pus_tm.pus_tm)
diff --git a/tests/test_queue.py b/tests/test_queue.py
index e888e3a6..da51c6ac 100644
--- a/tests/test_queue.py
+++ b/tests/test_queue.py
@@ -5,14 +5,19 @@
from unittest.mock import MagicMock
from spacepackets.ecss import PusTelecommand
+from spacepackets.seqcount import ProvidesSeqCount
+
from tmtccmd import DefaultProcedureInfo
-from tmtccmd.tmtc import WaitEntry, QueueEntryHelper
# Required for eval calls
# noinspection PyUnresolvedReferences
-from tmtccmd.tmtc import LogQueueEntry, RawTcEntry # noqa: F401
-from tmtccmd.tmtc.queue import QueueWrapper, DefaultPusQueueHelper
-from tmtccmd.util import ProvidesSeqCount
+from tmtccmd.tmtc import ( # noqa: F401
+ LogQueueEntry,
+ QueueEntryHelper,
+ RawTcEntry,
+ WaitEntry,
+)
+from tmtccmd.tmtc.queue import DefaultPusQueueHelper, QueueWrapper
class TestTcQueue(TestCase):
diff --git a/tests/test_seq_cnt_provider.py b/tests/test_seq_cnt_provider.py
deleted file mode 100644
index 38e796e8..00000000
--- a/tests/test_seq_cnt_provider.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import os
-from pathlib import Path
-import platform
-from unittest import TestCase
-
-from tmtccmd.util.seqcnt import PusFileSeqCountProvider
-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 = PusFileSeqCountProvider(Path(file.name))
- seq_cnt = seq_cnt_provider.current()
- self.assertEqual(seq_cnt, 0)
- # The first call will start at 0
- self.assertEqual(next(seq_cnt_provider), 0)
- self.assertEqual(seq_cnt_provider.get_and_increment(), 1)
- file.seek(0)
- file.write(f"{pow(2, 14) - 1}\n")
- file.flush()
- # Assert rollover
- self.assertEqual(next(seq_cnt_provider), pow(2, 14) - 1)
- self.assertEqual(next(seq_cnt_provider), 0)
-
- def test_with_real_file(self):
- seq_cnt_provider = PusFileSeqCountProvider(self.file_name)
- self.assertTrue(self.file_name.exists())
- self.assertEqual(seq_cnt_provider.current(), 0)
- self.assertEqual(next(seq_cnt_provider), 0)
- pass
-
- def test_file_deleted_runtime(self):
- seq_cnt_provider = PusFileSeqCountProvider(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 = PusFileSeqCountProvider(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 = PusFileSeqCountProvider(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/tmtccmd/cfdp/__init__.py b/tmtccmd/cfdp/__init__.py
index feba8a97..3e7be295 100644
--- a/tmtccmd/cfdp/__init__.py
+++ b/tmtccmd/cfdp/__init__.py
@@ -1,15 +1,5 @@
"""Please note that this module does not contain configuration helpers, for example
to convert CLI or GUI parameters into the internalized CFDP classes. You can find all those
helpers inside the :py:mod:`tmtccmd.config.cfdp` module."""
-from .defs import CfdpIndication, CfdpState
-from .request import CfdpRequestWrapper, PutRequest
-from .user import CfdpUserBase
-from .filestore import HostFilestore
-from .mib import (
- LocalEntityCfg,
- RemoteEntityCfgTable,
- RemoteEntityCfg,
- IndicationCfg,
-)
-from .handler.common import get_packet_destination, PacketDestination
+from .request import CfdpRequestWrapper
from spacepackets.cfdp import TransactionId
diff --git a/tmtccmd/cfdp/defs.py b/tmtccmd/cfdp/defs.py
deleted file mode 100644
index 07968b3a..00000000
--- a/tmtccmd/cfdp/defs.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from __future__ import annotations
-
-import enum
-
-
-class CfdpRequestType(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 CfdpState(enum.Enum):
- IDLE = 0
- BUSY = 1
- SUSPENDED = 2
-
-
-class SequenceNumberOverflow(Exception):
- pass
diff --git a/tmtccmd/cfdp/exceptions.py b/tmtccmd/cfdp/exceptions.py
deleted file mode 100644
index 5ce1a460..00000000
--- a/tmtccmd/cfdp/exceptions.py
+++ /dev/null
@@ -1,145 +0,0 @@
-import enum
-from pathlib import Path
-from spacepackets.util import UnsignedByteField
-from spacepackets.cfdp.defs import ChecksumType, Direction
-from spacepackets.cfdp import GenericPduPacket
-from spacepackets.cfdp.pdu import AbstractFileDirectiveBase
-
-
-class NoRemoteEntityCfgFound(Exception):
- def __init__(self, entity_id: UnsignedByteField, *args, **kwargs):
- super().__init__(args, kwargs)
- self.remote_entity_id = entity_id
-
- def __str__(self):
- return f"No remote entity found for entity ID {self.remote_entity_id}"
-
-
-class FsmNotCalledAfterPacketInsertion(Exception):
- def __init__(self):
- super().__init__("Call the state machine before inserting the next packet")
-
-
-class SourceFileDoesNotExist(Exception):
- def __init__(self, file: Path):
- self.file = file
- super().__init__(f"Source file {self.file} does not exist")
-
-
-class ChecksumNotImplemented(Exception):
- def __init__(self, checksum_type: ChecksumType):
- self.checksum_type = checksum_type
- super().__init__(f"{self.checksum_type} not implemented")
-
-
-class UnretrievedPdusToBeSent(Exception):
- pass
-
-
-class InvalidNakPdu(Exception):
- pass
-
-
-class InvalidPduDirection(Exception):
- def __init__(self, expected_dir: Direction, found_dir: Direction):
- self.expected_dir = expected_dir
- self.found_dir = found_dir
- super().__init__(
- f"Expected direction {self.expected_dir!r}, got {self.found_dir!r}"
- )
-
-
-class InvalidSourceId(Exception):
- """Invalid source entity ID. This is not necessarily the sender of a packet but actually the
- entity that started a transaction, or the entity which is transferring a file"""
-
- def __init__(
- self,
- expected_src_id: UnsignedByteField,
- found_src_id: UnsignedByteField,
- ):
- self.expected_src_id = expected_src_id
- self.found_src_id = found_src_id
- super().__init__(
- f"expected source {self.expected_src_id}, got {self.found_src_id}"
- )
-
-
-class InvalidDestinationId(Exception):
- """Invalid destination entity ID. This is not necessarily the receiver of a packet but actually
- the recipient of a file, or the entity receiving file data and metadata PDUs"""
-
- def __init__(
- self,
- expected_dest_id: UnsignedByteField,
- found_dest_id: UnsignedByteField,
- ):
- self.expected_dest_id = expected_dest_id
- self.found_dest_id = found_dest_id
- super().__init__(
- f"expected destination {self.expected_dest_id}, got {self.found_dest_id}"
- )
-
-
-class InvalidTransactionSeqNum(Exception):
- def __init__(self, expected: UnsignedByteField, received: UnsignedByteField):
- self.expected = expected
- self.received = received
- super().__init__(
- f"expected sequence number {expected}, reiceved {self.received}"
- )
-
-
-class BusyError(Exception):
- pass
-
-
-class InvalidPduForSourceHandler(Exception):
- def __init__(self, packet: AbstractFileDirectiveBase):
- self.packet = packet
- super().__init__(f"Invalid packet {self.packet} for source handler")
-
-
-class PduIgnoredForSourceReason(enum.IntEnum):
- # The received PDU can only be used for acknowledged mode.
- ACK_MODE_PACKET_INVALID_MODE = 0
- # Received a Finished PDU, but source handler is currently not expecting one.
- NOT_WAITING_FOR_FINISHED_PDU = 1
- # Received a ACK PDU, but source handler is currently not expecting one.
- NOT_WAITING_FOR_ACK = 2
-
-
-class PduIgnoredForSource(Exception):
- def __init__(
- self,
- reason: PduIgnoredForSourceReason,
- ignored_packet: AbstractFileDirectiveBase,
- ):
- self.ignored_packet = ignored_packet
- self.reason = reason
- super().__init__(f"ignored PDU packet at source handler: {reason!r}")
-
-
-class InvalidPduForDestHandler(Exception):
- def __init__(self, packet: GenericPduPacket):
- self.packet = packet
- super().__init__(f"Invalid packet {self.packet} for source handler")
-
-
-class PduIgnoredForDestReason(enum.IntEnum):
- FIRST_PACKET_NOT_METADATA_PDU = 0
- """First packet received was not a metadata PDU for the unacknowledged mode."""
- INVALID_MODE_FOR_ACKED_MODE_PACKET = 1
- """The received PDU can only be handled in acknowledged mode."""
- FIRST_PACKET_IN_ACKED_MODE_NOT_METADATA_NOT_EOF_NOT_FD = 2
- """For the acknowledged mode, the first packet that was received with
- no metadata received previously was not a File Data PDU or EOF PDU."""
-
-
-class PduIgnoredForDest(Exception):
- def __init__(
- self, reason: PduIgnoredForDestReason, ignored_packet: GenericPduPacket
- ):
- self.ignored_packet = ignored_packet
- self.reason = reason
- super().__init__(f"ignored PDU packet at destination handler: {reason!r}")
diff --git a/tmtccmd/cfdp/filestore.py b/tmtccmd/cfdp/filestore.py
deleted file mode 100644
index ab0b5252..00000000
--- a/tmtccmd/cfdp/filestore.py
+++ /dev/null
@@ -1,257 +0,0 @@
-import abc
-import logging
-import os
-import shutil
-import platform
-from pathlib import Path
-from typing import Optional, BinaryIO
-
-from spacepackets.cfdp.tlv import FilestoreResponseStatusCode
-
-_LOGGER = logging.getLogger(__name__)
-
-FilestoreResult = FilestoreResponseStatusCode
-
-
-class VirtualFilestore(abc.ABC):
- @abc.abstractmethod
- def read_data(self, file: Path, offset: Optional[int], read_len: int) -> bytes:
- """This is not used as part of a filestore request, it is used to read a file, for example
- to send it"""
- raise NotImplementedError("Reading file not implemented in virtual filestore")
-
- @abc.abstractmethod
- def read_from_opened_file(self, bytes_io: BinaryIO, offset: int, read_len: int):
- raise NotImplementedError(
- "Reading from opened file not implemented in virtual filestore"
- )
-
- @abc.abstractmethod
- def is_directory(self, path: Path) -> bool:
- pass
-
- @abc.abstractmethod
- def filename_from_full_path(self, path: Path) -> Optional[str]:
- pass
-
- @abc.abstractmethod
- def file_exists(self, path: Path) -> bool:
- pass
-
- @abc.abstractmethod
- def truncate_file(self, file: Path):
- pass
-
- @abc.abstractmethod
- def write_data(self, file: Path, data: bytes, offset: Optional[int]):
- """This is not used as part of a filestore request, it is used to build up the received
- file.
-
- :raises PermissionError:
- :raises FileNotFoundError:
- """
- raise NotImplementedError(
- "Writing to data not implemented in virtual filestore"
- )
-
- @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
-
- def read_data(
- self, file: Path, offset: Optional[int], read_len: Optional[int] = None
- ) -> bytes:
- if not file.exists():
- raise FileNotFoundError(file)
- file_size = file.stat().st_size
- if read_len is None:
- read_len = file_size
- if offset is None:
- offset = 0
- with open(file, "rb") as rf:
- rf.seek(offset)
- return rf.read(read_len)
-
- def read_from_opened_file(self, bytes_io: BinaryIO, offset: int, read_len: int):
- bytes_io.seek(offset)
- return bytes_io.read(read_len)
-
- def file_exists(self, path: Path) -> bool:
- return path.exists()
-
- def is_directory(self, path: Path) -> bool:
- return path.is_dir()
-
- def filename_from_full_path(self, path: Path) -> Optional[str]:
- return path.name
-
- def truncate_file(self, file: Path):
- if not file.exists():
- raise FileNotFoundError(file)
- with open(file, "w"):
- pass
-
- def write_data(self, file: Path, data: bytes, offset: Optional[int]):
- """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:
- :raises FileNotFoundError: File not found
- """
- if not file.exists():
- raise FileNotFoundError(file)
- with open(file, "r+b") as of:
- if offset is not None:
- of.seek(offset)
- of.write(data)
-
- 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/__init__.py b/tmtccmd/cfdp/handler/__init__.py
deleted file mode 100644
index fa01e8ea..00000000
--- a/tmtccmd/cfdp/handler/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from tmtccmd.util import ProvidesSeqCount
-from tmtccmd.cfdp.mib import (
- LocalEntityCfg,
- RemoteEntityCfgTable,
- RemoteEntityCfg,
-)
-
-from .dest import DestStateWrapper, DestHandler
-from .dest import TransactionStep as DestTransactionStep
-from .source import SourceHandler, SourceStateWrapper, FsmResult
-from .source import TransactionStep as SourceTransactionStep
-from .common import PacketDestination, get_packet_destination
diff --git a/tmtccmd/cfdp/handler/common.py b/tmtccmd/cfdp/handler/common.py
deleted file mode 100644
index 7532e200..00000000
--- a/tmtccmd/cfdp/handler/common.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import enum
-from dataclasses import dataclass
-from typing import Optional
-
-from spacepackets.cfdp import DirectiveType, GenericPduPacket, PduType
-from spacepackets.cfdp.pdu import PduHolder
-
-from tmtccmd.util.countdown import Countdown
-
-
-class PacketDestination(enum.Enum):
- SOURCE_HANDLER = 0
- DEST_HANDLER = 1
-
-
-def get_packet_destination(packet: GenericPduPacket) -> PacketDestination:
- """This function routes the packets based on PDU type and directive type if applicable.
-
- The routing is based on section 4.5 of the CFDP standard which specifies the PDU forwarding
- procedure.
-
- NOTE: The standard also specifies a direction flag, which could be used for that purpose as
- well. However, I prefer the approach here to explicitely check the PDU types and event the ACK
- PDU content. I think this is more reliable than relying on that bit.
- """
- if packet.pdu_type == PduType.FILE_DATA:
- return PacketDestination.DEST_HANDLER
- if packet.directive_type in [ # type: ignore
- DirectiveType.METADATA_PDU,
- DirectiveType.EOF_PDU,
- DirectiveType.PROMPT_PDU,
- ]:
- # Section b) of 4.5.3: These PDUs should always be targeted towards the file
- # receiver a.k.a. the destination handler
- return PacketDestination.DEST_HANDLER
- elif packet.directive_type in [ # type: ignore
- DirectiveType.FINISHED_PDU,
- DirectiveType.NAK_PDU,
- DirectiveType.KEEP_ALIVE_PDU,
- ]:
- # Section c) of 4.5.3: These PDUs should always be targeted towards the file sender
- # a.k.a. the source handler
- return PacketDestination.SOURCE_HANDLER
- elif packet.directive_type == DirectiveType.ACK_PDU: # type: ignore
- # Section a): Recipient depends on the type of PDU that is being acknowledged.
- # We can simply extract the PDU type from the raw stream. If it is an EOF PDU,
- # this packet is passed to the source handler. For a finished PDU, it is
- # passed to the destination handler
- pdu_holder = PduHolder(packet)
- ack_pdu = pdu_holder.to_ack_pdu()
- if ack_pdu.directive_code_of_acked_pdu == DirectiveType.EOF_PDU:
- return PacketDestination.SOURCE_HANDLER
- elif ack_pdu.directive_code_of_acked_pdu == DirectiveType.FINISHED_PDU:
- return PacketDestination.DEST_HANDLER
- raise ValueError(f"unexpected directive type {packet.directive_type}") # type: ignore
-
-
-@dataclass
-class _PositiveAckProcedureParams:
- ack_timer: Optional[Countdown] = None
- ack_counter: int = 0
diff --git a/tmtccmd/cfdp/handler/crc.py b/tmtccmd/cfdp/handler/crc.py
deleted file mode 100644
index 86e795c0..00000000
--- a/tmtccmd/cfdp/handler/crc.py
+++ /dev/null
@@ -1,79 +0,0 @@
-import struct
-from pathlib import Path
-from typing import Optional
-
-from crcmod.predefined import PredefinedCrc
-
-from spacepackets.cfdp import ChecksumType, NULL_CHECKSUM_U32
-from tmtccmd.cfdp.filestore import VirtualFilestore
-from tmtccmd.cfdp.exceptions import ChecksumNotImplemented, SourceFileDoesNotExist
-
-
-def calc_modular_checksum(file_path: Path) -> bytes:
- """Calculates the modular checksum for a file in one go."""
- checksum = 0
-
- with open(file_path, "rb") as file:
- while True:
- data = file.read(4)
- if not data:
- break
- checksum += int.from_bytes(
- data.ljust(4, b"\0"), byteorder="big", signed=False
- )
-
- checksum %= 2**32
- return struct.pack("!I", checksum)
-
-
-class CrcHelper:
- def __init__(self, init_type: ChecksumType, vfs: VirtualFilestore):
- self.checksum_type = init_type
- self.vfs = vfs
-
- def _verify_checksum(self):
- if self.checksum_type not in [
- ChecksumType.NULL_CHECKSUM,
- ChecksumType.CRC_32,
- ChecksumType.CRC_32C,
- ]:
- raise ChecksumNotImplemented(self.checksum_type)
-
- def checksum_type_to_crcmod_str(self) -> Optional[str]:
- if self.checksum_type == ChecksumType.NULL_CHECKSUM:
- return None
- if self.checksum_type == ChecksumType.CRC_32:
- return "crc32"
- elif self.checksum_type == ChecksumType.CRC_32C:
- return "crc32c"
-
- def generate_crc_calculator(self) -> PredefinedCrc:
- self._verify_checksum()
- return PredefinedCrc(self.checksum_type_to_crcmod_str())
-
- def calc_for_file(
- self, file_path: Path, file_sz: int, segment_len: int = 4096
- ) -> bytes:
- if self.checksum_type == ChecksumType.NULL_CHECKSUM:
- return NULL_CHECKSUM_U32
- elif self.checksum_type == ChecksumType.MODULAR:
- return calc_modular_checksum(file_path)
- crc_obj = self.generate_crc_calculator()
- if segment_len == 0:
- raise ValueError("Segment length can not be 0")
- if not file_path.exists():
- raise SourceFileDoesNotExist(file_path)
- current_offset = 0
- # Calculate the file CRC
- with open(file_path, "rb") as file:
- while current_offset < file_sz:
- if current_offset + segment_len > file_sz:
- read_len = file_sz - current_offset
- else:
- read_len = segment_len
- if read_len > 0:
- crc_obj.update(
- self.vfs.read_from_opened_file(file, current_offset, read_len)
- )
- current_offset += read_len
- return crc_obj.digest()
diff --git a/tmtccmd/cfdp/handler/defs.py b/tmtccmd/cfdp/handler/defs.py
deleted file mode 100644
index 6754388f..00000000
--- a/tmtccmd/cfdp/handler/defs.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from dataclasses import dataclass
-from typing import Optional
-
-
-@dataclass
-class _FileParamsBase:
- progress: int
- segment_len: int
- crc32: Optional[bytes]
- metadata_only: bool
- file_size: int
-
- @classmethod
- def empty(cls):
- return cls(
- progress=0, segment_len=0, crc32=None, file_size=0, metadata_only=False
- )
-
- def reset(self):
- self.progress = 0
- self.segment_len = 0
- self.crc32 = None
- self.file_size = 0
- self.metadata_only = False
diff --git a/tmtccmd/cfdp/handler/dest.py b/tmtccmd/cfdp/handler/dest.py
deleted file mode 100644
index 7da76f46..00000000
--- a/tmtccmd/cfdp/handler/dest.py
+++ /dev/null
@@ -1,1228 +0,0 @@
-from __future__ import annotations
-
-import enum
-import logging
-from collections import deque
-from dataclasses import dataclass
-from pathlib import Path
-from typing import Deque, List, Optional, Tuple
-
-from deprecated.sphinx import deprecated
-from spacepackets.cfdp import (
- ChecksumType,
- ConditionCode,
- Direction,
- FaultHandlerCode,
- PduConfig,
- PduType,
- TlvType,
- TransactionId,
- TransmissionMode,
-)
-from spacepackets.cfdp.pdu import (
- AckPdu,
- DirectiveType,
- EofPdu,
- FileDataPdu,
- FinishedPdu,
- MetadataPdu,
- NakPdu,
-)
-from spacepackets.cfdp.pdu.ack import TransactionStatus
-from spacepackets.cfdp.pdu.finished import DeliveryCode, FileStatus, FinishedParams
-from spacepackets.cfdp.pdu.helper import GenericPduPacket, PduHolder
-from spacepackets.cfdp.pdu.nak import get_max_seg_reqs_for_max_packet_size_and_pdu_cfg
-from spacepackets.cfdp.tlv import MessageToUserTlv
-from spacepackets.util import UnsignedByteField
-
-from tmtccmd.cfdp.defs import CfdpState
-from tmtccmd.cfdp.exceptions import (
- FsmNotCalledAfterPacketInsertion,
- InvalidDestinationId,
- InvalidPduDirection,
- InvalidPduForDestHandler,
- NoRemoteEntityCfgFound,
- PduIgnoredForDest,
- PduIgnoredForDestReason,
- UnretrievedPdusToBeSent,
-)
-from tmtccmd.cfdp.handler.common import (
- PacketDestination,
- _PositiveAckProcedureParams,
- get_packet_destination,
-)
-from tmtccmd.cfdp.handler.crc import CrcHelper
-from tmtccmd.cfdp.handler.defs import (
- _FileParamsBase,
-)
-from tmtccmd.cfdp.mib import (
- CheckTimerProvider,
- EntityType,
- LocalEntityCfg,
- RemoteEntityCfg,
- RemoteEntityCfgTable,
-)
-from tmtccmd.cfdp.user import (
- CfdpUserBase,
- FileSegmentRecvdParams,
- MetadataRecvParams,
- TransactionFinishedParams,
-)
-from tmtccmd.util.countdown import Countdown
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class CompletionDisposition(enum.Enum):
- COMPLETED = 0
- CANCELLED = 1
-
-
-@dataclass
-class _DestFileParams(_FileParamsBase):
- file_name: Path
- file_size_eof: Optional[int]
- condition_code_eof: Optional[ConditionCode]
-
- @classmethod
- def empty(cls) -> _DestFileParams:
- return cls(
- progress=0,
- segment_len=0,
- crc32=bytes(),
- file_size=0,
- file_name=Path(),
- file_size_eof=None,
- metadata_only=False,
- condition_code_eof=None,
- )
-
- def reset(self):
- super().reset()
- self.file_name = Path()
- self.file_size_eof = None
- self.condition_code_eof = None
-
-
-class TransactionStep(enum.Enum):
- IDLE = 0
- TRANSACTION_START = 1
- """Metadata was received, which triggered a transaction start."""
- WAITING_FOR_METADATA = 2
- """Special state which is only used for acknowledged mode. The CFDP entity is still waiting
- for a missing metadata PDU to be re-sent. Until then, all arriving file data PDUs will only
- update the internal lost segment tracker. If the EOF PDU is arrive, the state will go to.
- Please note that deferred lost segment handling might also be active when this state is set."""
- RECEIVING_FILE_DATA = 3
- RECV_FILE_DATA_WITH_CHECK_LIMIT_HANDLING = 4
- """This is the check timer step as specified in chapter 4.6.3.3 b) of the standard.
- The destination handler will still check for file data PDUs which might lead to a full
- file transfer completion."""
- SENDING_EOF_ACK_PDU = 5
- """Sending the ACK (EOF) packet."""
- WAITING_FOR_MISSING_DATA = 6
- """Only relevant for acknowledged mode: Wait for lost metadata and file segments as part of
- the deferred lost segments detection procedure."""
- TRANSFER_COMPLETION = 7
- """File transfer complete. Perform checksum verification and notice of completion. Please
- note that this does not necessarily mean that the file transfer was completed succesfully."""
- SENDING_FINISHED_PDU = 8
- WAITING_FOR_FINISHED_ACK = 9
-
-
-@dataclass
-class DestStateWrapper:
- state: CfdpState = CfdpState.IDLE
- step: TransactionStep = TransactionStep.IDLE
- transaction_id: Optional[TransactionId] = None
- _num_packets_ready: int = 0
-
- @property
- def num_packets_ready(self) -> int:
- return self._num_packets_ready
-
- @property
- def packets_ready(self) -> bool:
- return self.num_packets_ready > 0
-
-
-class LostSegmentTracker:
- def __init__(self):
- self.lost_segments = {}
-
- @property
- def num_lost_segments(self):
- return len(self.lost_segments)
-
- def reset(self):
- self.lost_segments.clear()
-
- def add_lost_segment(self, lost_seg: Tuple[int, int]):
- self.lost_segments.update({lost_seg[0]: lost_seg[1]})
- self.lost_segments = dict(sorted(self.lost_segments.items()))
-
- def coalesce_lost_segments(self):
- if len(self.lost_segments) <= 1:
- return
- merged_segments = []
- current_start, current_end = next(iter(self.lost_segments.items()))
-
- for seg_start, seg_end in self.lost_segments.items():
- if seg_start == current_end:
- current_end = seg_end
- else:
- merged_segments.append((current_start, current_end))
- current_start, current_end = seg_start, seg_end
-
- merged_segments.append((current_start, current_end))
- self.lost_segments = {start: end for (start, end) in merged_segments}
-
- def remove_lost_segment(self, segment_to_remove: Tuple[int, int]) -> bool:
- """Please note that this method can only handle the removal of segments
- which do not overlap the boundaries of an existing lost segment. It is however able
- to remove lost segments which are only a subset of an existing section.
-
- Returns
- ---------
-
- Returns whether the internal dictionary was manipulated in any way.
- """
- if segment_to_remove[1] - segment_to_remove[0] == 0:
- return False
- did_something = False
- end = self.lost_segments.get(segment_to_remove[0])
- if end is not None:
- if segment_to_remove[1] > end:
- raise ValueError(
- "Specified lost segment end exceeds existing lost segment end"
- )
- did_something = True
- if segment_to_remove[1] == end:
- self.lost_segments.pop(segment_to_remove[0])
- elif segment_to_remove[1] < end:
- self.lost_segments.pop(segment_to_remove[0])
- # Re-insert the rest of the missing segment
- self.lost_segments.update({segment_to_remove[1]: end})
- else:
- for seg_start, seg_end in list(self.lost_segments.items()):
- if seg_start < segment_to_remove[0] < seg_end:
- if segment_to_remove[1] > seg_end:
- raise ValueError(
- "Specified lost segment end exceeds existing lost segment end"
- )
- if segment_to_remove[1] == seg_end:
- self.lost_segments.update({seg_start: segment_to_remove[0]})
- else:
- self.lost_segments.update({seg_start: segment_to_remove[0]})
- self.lost_segments.update({segment_to_remove[1]: seg_end})
- did_something = True
- break
- if did_something:
- self.lost_segments = dict(sorted(self.lost_segments.items()))
- return did_something
-
-
-@dataclass
-class _AckedModeParams:
- lost_seg_tracker: LostSegmentTracker = LostSegmentTracker()
- metadata_missing: bool = False
- last_start_offset: int = 0
- last_end_offset: int = 0
- deferred_lost_segment_detection_active: bool = False
- procedure_timer: Optional[Countdown] = None
- nak_activity_counter: int = 0
-
-
-class _DestFieldWrapper:
- """Private wrapper class for internal use only."""
-
- def __init__(self):
- self.transaction_id: Optional[TransactionId] = None
- self.remote_cfg: Optional[RemoteEntityCfg] = None
- self.check_timer: Optional[Countdown] = None
- self.current_check_count: int = 0
- self.closure_requested: bool = False
- self.finished_params: FinishedParams = FinishedParams(
- delivery_code=DeliveryCode.DATA_INCOMPLETE,
- file_status=FileStatus.FILE_STATUS_UNREPORTED,
- condition_code=ConditionCode.NO_ERROR,
- )
- self.completion_disposition: CompletionDisposition = (
- CompletionDisposition.COMPLETED
- )
- self.pdu_conf = PduConfig.empty()
- self.fp: _DestFileParams = _DestFileParams.empty()
-
- self.acked_params = _AckedModeParams()
- self.positive_ack_params = _PositiveAckProcedureParams()
- self.last_inserted_packet = PduHolder(None)
-
- def reset(self):
- self.transaction_id = None
- self.closure_requested = False
- self.pdu_conf = PduConfig.empty()
- self.finished_params = FinishedParams(
- condition_code=ConditionCode.NO_ERROR,
- delivery_code=DeliveryCode.DATA_INCOMPLETE,
- file_status=FileStatus.FILE_STATUS_UNREPORTED,
- )
- self.finished_params.file_status = FileStatus.FILE_STATUS_UNREPORTED
- self.completion_disposition = CompletionDisposition.COMPLETED
- self.fp.reset()
- self.acked_params = _AckedModeParams()
- self.remote_cfg = None
- self.last_inserted_packet.pdu = None
- self.current_check_count = 0
-
-
-class FsmResult:
- def __init__(self, states: DestStateWrapper):
- self.states = states
-
-
-def acknowledge_inactive_eof_pdu(eof_pdu: EofPdu, status: TransactionStatus) -> AckPdu:
- """This function can be used to fulfill chapter 4.7.2 of the CFDP standard: Every EOF PDU
- received from the CFDP sender entity MUST be acknowledged, even if the transaction ID of
- the EOF PDU is not active at the receiver entity. The
- :py:class:`spacepackets.cfdp.pdu.ack.TransactionStatus` is user provided with the following
- options:
-
- 1. ``UNDEFINED``: The CFDP implementation does not retain a transaction history, so it might
- have been formerly active and terminated since then, or never active at all.
- 2. ``TERMINATED``: The CFDP implementation does retain a transaction history and is known
- to have been active at this entity.
- 3. ``UNRECOGNIZED``: The CFDP implementation does retain a transaction history and has never been
- active at this entity.
-
- See the :py:class:`tmtccmd.cfdp.user.CfdpUserBase` and the documentation for a possible way to
- keep a transaction history.
- """
- if status == TransactionStatus.ACTIVE:
- raise ValueError("invalid transaction status for inactive transaction")
- pdu_conf = eof_pdu.pdu_header.pdu_conf
- pdu_conf.direction = Direction.TOWARDS_SENDER
- return AckPdu(pdu_conf, DirectiveType.EOF_PDU, eof_pdu.condition_code, status)
-
-
-class DestHandler:
- """This is the primary CFDP destination handler. It models the CFDP destination entity, which
- is primarily responsible for receiving files sent from another CFDP entity. It performs the
- reception side of File Copy Operations.
-
- This handler supports both acknowledged and unacknowledged CFDP file transfers.
- The following core functions are the primary interface for interacting with the destination
- handler:
-
- 1. :py:meth:`insert_packet` : Can be used to insert packets into the destination
- handler. Please note that the destination handler can also only process Metadata, EOF and
- Prompt PDUs in addition to ACK PDUs where the acknowledged PDU is the Finished PDU.
- Right now, the handler processes one packet at a time, and each packer insertion needs
- to be followed by a :py:meth:`state_machine` call.
- 2. :py:meth:`state_machine` : This state machine processes inserted packets while also
- generating the packets which need to be sent back to the initiator of a file copy
- operation.
- 3. :py:meth:`get_next_packet`: Retrieve next packet to be sent back to the remote CFDP source
- entity ID.
-
- A new file transfer (Metadata PDU reception) is only be accepted if the handler is in the IDLE
- state. Furthermore, packet insertion is not allowed until all packets to send were retrieved
- after a state machine call."""
-
- def __init__(
- self,
- cfg: LocalEntityCfg,
- user: CfdpUserBase,
- remote_cfg_table: RemoteEntityCfgTable,
- check_timer_provider: CheckTimerProvider,
- ):
- self.cfg = cfg
- self.remote_cfg_table = remote_cfg_table
- self.states = DestStateWrapper()
- self.user = user
- self.check_timer_provider = check_timer_provider
- self._params = _DestFieldWrapper()
- self._cksum_verif_helper: CrcHelper = CrcHelper(
- ChecksumType.NULL_CHECKSUM, user.vfs
- )
- self._pdus_to_be_sent: Deque[PduHolder] = deque()
-
- @property
- def entity_id(self) -> UnsignedByteField:
- return self.cfg.local_entity_id
-
- @property
- def closure_requested(self) -> bool:
- """Returns whether a closure was requested for the current transaction. Please note that
- this variable is only valid as long as the state is not IDLE"""
- return self._params.closure_requested
-
- @property
- def transmission_mode(self) -> Optional[TransmissionMode]:
- if self.states.state == CfdpState.IDLE:
- return None
- return self._params.pdu_conf.trans_mode
-
- @property
- def state(self) -> CfdpState:
- return self.states.state
-
- @property
- def step(self) -> TransactionStep:
- return self.states.step
-
- @property
- def transaction_id(self) -> Optional[TransactionId]:
- return self._params.transaction_id
-
- @property
- def current_check_counter(self) -> int:
- """This is the check count used for the check limit mechanism for incomplete unacknowledged
- file transfers. A Check Limit Reached fault will be declared once this check counter
- reaches the configured check limit. More information can be found in chapter 4.6.3.3 b) of
- the standard."""
- return self._params.current_check_count
-
- @property
- def deferred_lost_segment_procedure_active(self) -> bool:
- return self._params.acked_params.deferred_lost_segment_detection_active
-
- @property
- def nak_activity_counter(self) -> int:
- return self._params.acked_params.nak_activity_counter
-
- @property
- def positive_ack_counter(self) -> int:
- return self._params.positive_ack_params.ack_counter
-
- @property
- def packets_ready(self) -> bool:
- return self.states.packets_ready
-
- @property
- def num_packets_ready(self) -> int:
- return self.states.num_packets_ready
-
- def state_machine(self) -> FsmResult:
- """This is the primary call to run the state machine after packet insertion and/or after
- having sent any packets which need to be sent to the sender of a file transaction.
- """
- if self.states.state == CfdpState.IDLE:
- self.__idle_fsm()
- # Calling the FSM immediately would lead to an exception, user must send any PDUs which
- # might have been generated (e.g. NAK PDUs to re-request metadata) first.
- if self.packets_ready:
- return FsmResult(self.states)
- if self.states.state == CfdpState.BUSY:
- self.__non_idle_fsm()
- return FsmResult(self.states)
-
- def insert_packet(self, packet: GenericPduPacket):
- """Insert a packet into the state machine. The packet will be processed with the
- next :py:meth:`state_machine` call, which might lead to state machine transitions
- and/or new packets generated, which need to be sent to the file sender for the
- corresponding file transaction.
-
- :raise NoRemoteEntityCfgFound: No remote configuration found for source entity ID
- extracted from the PDU packet.
- :raise FsmNotCalledAfterPacketInsertion: :py:meth:`state_machine` needs to be called
- to clear a previously inserted packet.
- :raise InvalidPduDirection: PDU direction bit is invalid.
- :raise InvalidDestinationId: The PDU destination entity ID is not equal to the configured
- local ID.
- :raise InvalidPduForDestHandler: The PDU type can not be handled by the destination handler
- :raise PduIgnoredForDest: The PDU was ignored because it can not be handled for the current
- transmission mode or internal state.
- """
- if self._params.last_inserted_packet.pdu is not None:
- raise FsmNotCalledAfterPacketInsertion()
- if packet.direction != Direction.TOWARDS_RECEIVER:
- raise InvalidPduDirection(
- Direction.TOWARDS_RECEIVER, packet.pdu_header.direction
- )
- if packet.dest_entity_id != self.cfg.local_entity_id:
- raise InvalidDestinationId(
- self.cfg.local_entity_id, packet.source_entity_id
- )
- if self.remote_cfg_table.get_cfg(packet.source_entity_id) is None:
- raise NoRemoteEntityCfgFound(entity_id=packet.dest_entity_id)
- if get_packet_destination(packet) == PacketDestination.SOURCE_HANDLER:
- raise InvalidPduForDestHandler(packet)
- if (self.states.state == CfdpState.IDLE) and (
- packet.pdu_type == PduType.FILE_DATA
- or packet.directive_type != DirectiveType.METADATA_PDU # type: ignore
- ):
- self._handle_first_packet_not_metadata_pdu(packet)
- if packet.pdu_type == PduType.FILE_DIRECTIVE and (
- packet.directive_type # type: ignore
- in [DirectiveType.ACK_PDU, DirectiveType.PROMPT_PDU]
- and self.states.state == CfdpState.BUSY
- and self.transmission_mode == TransmissionMode.UNACKNOWLEDGED
- ):
- raise PduIgnoredForDest(
- PduIgnoredForDestReason.INVALID_MODE_FOR_ACKED_MODE_PACKET, packet
- )
- self._params.last_inserted_packet.pdu = packet
-
- def get_next_packet(self) -> Optional[PduHolder]:
- """Retrieve the next packet which should be sent to the remote CFDP source entity."""
- if len(self._pdus_to_be_sent) == 0:
- return None
- self.states._num_packets_ready -= 1
- return self._pdus_to_be_sent.popleft()
-
- def cancel_request(self, transaction_id: TransactionId) -> bool:
- """This function models the Cancel.request CFDP primtive and is the recommended way
- to cancel a transaction. It will cause a Notice Of Cancellation at this entity.
- Please note that the state machine might still be active because a canceled transfer
- might still require some packets to be sent to the remote sender entity.
-
- Returns
- --------
- True
- Current transfer was cancelled
- False
- The state machine is in the IDLE state or there is a transaction ID missmatch.
- """
- if self.states.state == CfdpState.IDLE:
- return False
- if self.states.packets_ready:
- raise UnretrievedPdusToBeSent()
- if (
- self._params.transaction_id is not None
- and transaction_id == self._params.transaction_id
- ):
- self._trigger_notice_of_completion_canceled(
- ConditionCode.CANCEL_REQUEST_RECEIVED
- )
- return True
- return False
-
- def reset(self):
- """This function is public to allow completely resetting the handler, but it is explicitely
- discouraged to do this. CFDP generally has mechanism to detect issues and errors on itself.
- """
- self._params.reset()
- self._pdus_to_be_sent.clear()
- self.states.state = CfdpState.IDLE
- self.states.step = TransactionStep.IDLE
-
- def __idle_fsm(self):
- if self._params.last_inserted_packet.pdu is None:
- return
- if self._params.last_inserted_packet.pdu_type == PduType.FILE_DATA:
- file_data_pdu = self._params.last_inserted_packet.to_file_data_pdu()
- self._start_transaction_missing_metadata_recv_fd(file_data_pdu)
- else:
- assert self._params.last_inserted_packet.pdu_directive_type is not None
- if (
- self._params.last_inserted_packet.pdu_directive_type
- == DirectiveType.EOF_PDU
- ):
- eof_pdu = self._params.last_inserted_packet.to_eof_pdu()
- self._start_transaction_missing_metadata_recv_eof(eof_pdu)
- elif (
- self._params.last_inserted_packet.pdu_directive_type
- == DirectiveType.METADATA_PDU
- ):
- metadata_pdu = self._params.last_inserted_packet.to_metadata_pdu()
- self._start_transaction(metadata_pdu)
- else:
- raise ValueError(
- f"unexpected configuration error: {self._params.last_inserted_packet.pdu} in "
- f"IDLE state machine"
- )
- self._params.last_inserted_packet.pdu = None
-
- def __non_idle_fsm(self):
- self._fsm_advancement_after_packets_were_sent()
- if self.states.step in [
- TransactionStep.RECEIVING_FILE_DATA,
- TransactionStep.RECV_FILE_DATA_WITH_CHECK_LIMIT_HANDLING,
- ]:
- if self._params.last_inserted_packet.pdu is not None:
- self._handle_fd_or_eof_pdu()
- self._params.last_inserted_packet.pdu = None
- if self.states.step == TransactionStep.WAITING_FOR_METADATA:
- self._handle_waiting_for_missing_metadata()
- self._deferred_lost_segment_handling()
- if self.states.step == TransactionStep.RECV_FILE_DATA_WITH_CHECK_LIMIT_HANDLING:
- self._check_limit_handling()
- if self.states.step == TransactionStep.WAITING_FOR_MISSING_DATA:
- if (
- self._params.last_inserted_packet.pdu is not None
- and self._params.last_inserted_packet.pdu_type == PduType.FILE_DATA
- ):
- self._handle_fd_pdu(
- self._params.last_inserted_packet.to_file_data_pdu()
- )
- if self._params.acked_params.deferred_lost_segment_detection_active:
- self._reset_nak_activity_parameters()
- self._params.last_inserted_packet.pdu = None
- self._deferred_lost_segment_handling()
- if self.states.step == TransactionStep.TRANSFER_COMPLETION:
- self._handle_transfer_completion()
- if self.states.step == TransactionStep.SENDING_FINISHED_PDU:
- self._prepare_finished_pdu()
- if self.states.step == TransactionStep.WAITING_FOR_FINISHED_ACK:
- self._handle_waiting_for_finished_ack()
-
- def _fsm_advancement_after_packets_were_sent(self):
- """Advance the internal FSM after all packets to be sent were retrieved from the handler."""
- if len(self._pdus_to_be_sent) > 0:
- raise UnretrievedPdusToBeSent(
- f"{len(self._pdus_to_be_sent)} packets left to send"
- )
- if self.states.step == TransactionStep.SENDING_FINISHED_PDU:
- if (
- self.states.state == CfdpState.BUSY
- and self.transmission_mode == TransmissionMode.ACKNOWLEDGED
- ):
- self._start_positive_ack_procedure()
- self.states.step = TransactionStep.WAITING_FOR_FINISHED_ACK
- return
- self.reset()
- if self.states.step == TransactionStep.SENDING_EOF_ACK_PDU:
- if (
- self._params.acked_params.lost_seg_tracker.num_lost_segments > 0
- or self._params.acked_params.metadata_missing
- ):
- self._start_deferred_lost_segment_handling()
- else:
- if (
- self._params.completion_disposition
- != CompletionDisposition.CANCELLED
- ):
- self._checksum_verify()
- self.states.step = TransactionStep.TRANSFER_COMPLETION
-
- def _start_transaction(self, metadata_pdu: MetadataPdu) -> bool:
- if self.states.state != CfdpState.IDLE:
- return False
- self._params.reset()
- self._common_first_packet_handler(metadata_pdu)
- self._handle_metadata_packet(metadata_pdu)
- return True
-
- def _handle_first_packet_not_metadata_pdu(self, packet: GenericPduPacket):
- if packet.transmission_mode == TransmissionMode.UNACKNOWLEDGED:
- raise PduIgnoredForDest(
- PduIgnoredForDestReason.FIRST_PACKET_NOT_METADATA_PDU, packet
- )
- elif packet.transmission_mode == TransmissionMode.ACKNOWLEDGED:
- if (
- packet.pdu_type == PduType.FILE_DIRECTIVE
- and packet.directive_type != DirectiveType.EOF_PDU # type: ignore
- ):
- raise PduIgnoredForDest(
- PduIgnoredForDestReason.FIRST_PACKET_IN_ACKED_MODE_NOT_METADATA_NOT_EOF_NOT_FD,
- packet,
- )
-
- def _start_transaction_missing_metadata_recv_eof(self, eof_pdu: EofPdu):
- self._common_first_packet_not_metadata_pdu_handler(eof_pdu)
- self._handle_eof_without_previous_metadata(eof_pdu)
-
- def _handle_eof_without_previous_metadata(self, eof_pdu: EofPdu):
- self._params.fp.progress = eof_pdu.file_size
- self._params.fp.file_size_eof = eof_pdu.file_size
- self._params.fp.condition_code_eof = eof_pdu.condition_code
- self._params.acked_params.metadata_missing = True
- if self._params.fp.progress > 0:
- # Clear old list, deferred procedure for the whole file is now active.
- self._params.acked_params.lost_seg_tracker.reset()
- # I will just wait until the metadata has been received with re-requesting the file
- # data PDU. How does the standard expect me to process file data PDUs where I do not
- # even know the filenames? How would I even generically do this? I will add the whole
- # file to the lost segments map for now.
- self._params.acked_params.lost_seg_tracker.add_lost_segment(
- (0, eof_pdu.file_size)
- )
- if self.cfg.indication_cfg.eof_recv_indication_required:
- assert self._params.transaction_id is not None
- self.user.eof_recv_indication(self._params.transaction_id)
- self._prepare_eof_ack_packet()
- self.states.step = TransactionStep.SENDING_EOF_ACK_PDU
-
- def _start_transaction_missing_metadata_recv_fd(self, fd_pdu: FileDataPdu):
- self._common_first_packet_not_metadata_pdu_handler(fd_pdu)
- self._handle_fd_without_previous_metadata(True, fd_pdu)
-
- def _handle_fd_without_previous_metadata(
- self, first_pdu: bool, fd_pdu: FileDataPdu
- ):
- self._params.fp.progress = fd_pdu.offset + len(fd_pdu.file_data)
- if len(fd_pdu.file_data) > 0:
- start = fd_pdu.offset
- if first_pdu:
- start = 0
- # I will just wait until the metadata has been received with re-requesting the file
- # data PDU. How does the standard expect me to process file data PDUs where I do not
- # even know the filenames? How would I even generically do this?
- # I will add this file segment (and all others which came before and might be missing
- # as well) to the lost segment list.
- self._params.acked_params.lost_seg_tracker.add_lost_segment(
- (start, self._params.fp.progress)
- )
- # This is a bit tricky: We need to set those variables to an appropriate value so
- # the removal of handled lost segments works properly. However, we can not set the
- # start offset to the regular value because we have to treat the current segment
- # like a lost segment as well.
- self._params.acked_params.last_start_offset = self._params.fp.progress
- self._params.acked_params.last_end_offset = self._params.fp.progress
- assert self._params.remote_cfg is not None
- # Re-request the metadata PDU.
- if self._params.remote_cfg.immediate_nak_mode:
- lost_segments: List[Tuple[int, int]] = []
- if first_pdu:
- lost_segments.append((0, 0))
- if len(fd_pdu.file_data) > 0:
- lost_segments.append((0, self._params.fp.progress))
- if len(lost_segments) > 0:
- self._add_packet_to_be_sent(
- NakPdu(
- self._params.pdu_conf,
- start_of_scope=0,
- end_of_scope=self._params.fp.progress,
- segment_requests=lost_segments,
- )
- )
-
- def _common_first_packet_not_metadata_pdu_handler(self, pdu: GenericPduPacket):
- self._params.reset()
- self._common_first_packet_handler(pdu)
- self.states.step = TransactionStep.WAITING_FOR_METADATA
- self._params.acked_params.metadata_missing = True
-
- def _common_first_packet_handler(self, pdu: GenericPduPacket):
- if self.states.state != CfdpState.IDLE:
- return False
- self.states.state = CfdpState.BUSY
- self._params.pdu_conf = pdu.pdu_header.pdu_conf
- self._params.pdu_conf.direction = Direction.TOWARDS_SENDER
- self._params.transaction_id = TransactionId(
- source_entity_id=pdu.source_entity_id,
- transaction_seq_num=pdu.transaction_seq_num,
- )
- self.states.transaction_id = self._params.transaction_id
- self._params.remote_cfg = self.remote_cfg_table.get_cfg(pdu.source_entity_id)
-
- def _handle_metadata_packet(self, metadata_pdu: MetadataPdu):
- self._cksum_verif_helper.checksum_type = metadata_pdu.checksum_type
- self._params.closure_requested = metadata_pdu.closure_requested
- self._params.acked_params.metadata_missing = False
- if metadata_pdu.dest_file_name is None or metadata_pdu.source_file_name is None:
- self._params.fp.metadata_only = True
- self._params.finished_params.delivery_code = DeliveryCode.DATA_COMPLETE
- else:
- self._params.fp.file_name = Path(metadata_pdu.dest_file_name)
- self._params.fp.file_size = metadata_pdu.file_size
- # To be fully standard-compliant or at least allow the flexibility to be standard-compliant
- # in the future, we should require that a remote entity configuration exists for each CFDP
- # sender.
- if self._params.remote_cfg is None:
- _LOGGER.warning(
- "No remote configuration found for remote ID"
- f" {metadata_pdu.dest_entity_id}"
- )
- raise NoRemoteEntityCfgFound(metadata_pdu.dest_entity_id)
- if not self._params.fp.metadata_only:
- self.states.step = TransactionStep.RECEIVING_FILE_DATA
- self._init_vfs_handling(Path(metadata_pdu.source_file_name).name) # type: ignore
- else:
- self.states.step = TransactionStep.TRANSFER_COMPLETION
- msgs_to_user_list = None
- if metadata_pdu.options is not None:
- msgs_to_user_list = []
- for tlv in metadata_pdu.options:
- if tlv.tlv_type == TlvType.MESSAGE_TO_USER:
- msgs_to_user_list.append(MessageToUserTlv.from_tlv(tlv))
- file_size_for_indication = (
- None if metadata_pdu.source_file_name is None else metadata_pdu.file_size
- )
- params = MetadataRecvParams(
- transaction_id=self._params.transaction_id, # type: ignore
- file_size=file_size_for_indication,
- source_id=metadata_pdu.source_entity_id,
- dest_file_name=metadata_pdu.dest_file_name,
- source_file_name=metadata_pdu.source_file_name,
- msgs_to_user=msgs_to_user_list,
- )
- self.user.metadata_recv_indication(params)
-
- def _init_vfs_handling(self, source_base_name: str):
- try:
- # If the destination is a directory, append the base name to the directory
- # Example: For source path /tmp/hello.txt and dest path /tmp, build /tmp/hello.txt for
- # the destination.
- if self.user.vfs.is_directory(self._params.fp.file_name):
- self._params.fp.file_name = self._params.fp.file_name.joinpath(
- source_base_name
- )
- if self.user.vfs.file_exists(self._params.fp.file_name):
- self.user.vfs.truncate_file(self._params.fp.file_name)
- else:
- self.user.vfs.create_file(self._params.fp.file_name)
- self._params.finished_params.file_status = FileStatus.FILE_RETAINED
- except PermissionError:
- self._params.finished_params.file_status = (
- FileStatus.DISCARDED_FILESTORE_REJECTION
- )
- self._declare_fault(ConditionCode.FILESTORE_REJECTION)
-
- def _handle_fd_or_eof_pdu(self):
- """Returns whether to exit the FSM prematurely."""
- if self._params.last_inserted_packet.pdu.pdu_type == PduType.FILE_DATA: # type: ignore
- self._handle_fd_pdu(self._params.last_inserted_packet.to_file_data_pdu())
- elif (
- self._params.last_inserted_packet.pdu.directive_type # type: ignore
- == DirectiveType.EOF_PDU
- ):
- self._handle_eof_pdu(self._params.last_inserted_packet.to_eof_pdu())
-
- def _handle_waiting_for_missing_metadata(self):
- if self._params.last_inserted_packet.pdu is None:
- return
- if self._params.last_inserted_packet.pdu.pdu_type == PduType.FILE_DATA:
- self._handle_fd_without_previous_metadata(
- True, self._params.last_inserted_packet.to_file_data_pdu()
- )
- elif (
- self._params.last_inserted_packet.pdu.directive_type # type: ignore
- == DirectiveType.METADATA_PDU
- ):
- self._handle_metadata_packet(
- self._params.last_inserted_packet.to_metadata_pdu()
- )
- if self._params.acked_params.deferred_lost_segment_detection_active:
- self._reset_nak_activity_parameters()
- elif (
- self._params.last_inserted_packet.pdu.directive_type # type: ignore
- == DirectiveType.EOF_PDU
- ):
- self._handle_eof_without_previous_metadata(
- self._params.last_inserted_packet.to_eof_pdu()
- )
- if self._params.acked_params.deferred_lost_segment_detection_active:
- self._reset_nak_activity_parameters()
- self._params.last_inserted_packet.pdu = None
-
- def _reset_nak_activity_parameters(self):
- assert self._params.acked_params.procedure_timer is not None
- self._params.acked_params.nak_activity_counter = 0
- self._params.acked_params.procedure_timer.reset()
-
- def _handle_waiting_for_finished_ack(self):
- """Returns False if the FSM should be called again."""
- if (
- self._params.last_inserted_packet.pdu is None
- or self._params.last_inserted_packet.pdu_type == PduType.FILE_DATA
- or self._params.last_inserted_packet.pdu_directive_type
- != DirectiveType.ACK_PDU
- ):
- self._handle_positive_ack_procedures()
- return
- if (
- self._params.last_inserted_packet.pdu_type == PduType.FILE_DIRECTIVE
- and self._params.last_inserted_packet.pdu_directive_type
- == DirectiveType.ACK_PDU
- ):
- ack_pdu = self._params.last_inserted_packet.to_ack_pdu()
- if ack_pdu.directive_code_of_acked_pdu != DirectiveType.FINISHED_PDU:
- _LOGGER.warn(
- f"received ACK PDU with invalid ACKed directive code "
- f" {ack_pdu.directive_code_of_acked_pdu}"
- )
- # We are done.
- self.reset()
-
- def _handle_positive_ack_procedures(self):
- """Positive ACK procedures according to chapter 4.7.1 of the CFDP standard.
- Returns False if the FSM should be called again."""
- assert self._params.positive_ack_params.ack_timer is not None
- assert self._params.remote_cfg is not None
- if self._params.positive_ack_params.ack_timer.timed_out():
- if (
- self._params.positive_ack_params.ack_counter + 1
- >= self._params.remote_cfg.positive_ack_timer_expiration_limit
- ):
- self._declare_fault(ConditionCode.POSITIVE_ACK_LIMIT_REACHED)
- # This is a bit of a hack: We want the transfer completion and the corresponding
- # re-send of the Finished PDU to happen in the same FSM cycle. However, the call
- # order in the FSM prevents this from happening, so we just call the state machine
- # again manually.
- if (
- self._params.completion_disposition
- == CompletionDisposition.CANCELLED
- ):
- return self.state_machine()
- self._params.positive_ack_params.ack_timer.reset()
- self._params.positive_ack_params.ack_counter += 1
- self._prepare_finished_pdu()
-
- def _handle_fd_pdu(self, file_data_pdu: FileDataPdu):
- data = file_data_pdu.file_data
- offset = file_data_pdu.offset
- if self.cfg.indication_cfg.file_segment_recvd_indication_required:
- file_segment_indic_params = FileSegmentRecvdParams(
- transaction_id=self._params.transaction_id, # type: ignore
- length=len(file_data_pdu.file_data),
- offset=offset,
- segment_metadata=file_data_pdu.segment_metadata,
- )
- self.user.file_segment_recv_indication(file_segment_indic_params)
- try:
- next_expected_progress = offset + len(data)
- if self.transmission_mode == TransmissionMode.ACKNOWLEDGED:
- self._lost_segment_handling(offset, len(data))
- self.user.vfs.write_data(self._params.fp.file_name, data, offset)
- self._params.finished_params.file_status = FileStatus.FILE_RETAINED
-
- if self._params.fp.file_size_eof is not None and (
- offset + len(file_data_pdu.file_data) > self._params.fp.file_size_eof
- ):
- # CFDP 4.6.1.2.7 c): If the sum of the FD PDU offset and segment size exceeds
- # the file size indicated in the first previously received EOF (No Error) PDU, if
- # any, then then a File Size Error fault shall be declared.
- if (
- self._declare_fault(ConditionCode.FILE_SIZE_ERROR)
- != FaultHandlerCode.IGNORE_ERROR
- ):
- return
- # Ensure that the progress value is always incremented
- if next_expected_progress > self._params.fp.progress:
- self._params.fp.progress = next_expected_progress
- except FileNotFoundError:
- if self._params.finished_params.file_status != FileStatus.FILE_RETAINED:
- self._params.finished_params.file_status = (
- FileStatus.DISCARDED_FILESTORE_REJECTION
- )
- self._declare_fault(ConditionCode.FILESTORE_REJECTION)
- except PermissionError:
- if self._params.finished_params.file_status != FileStatus.FILE_RETAINED:
- self._params.finished_params.file_status = (
- FileStatus.DISCARDED_FILESTORE_REJECTION
- )
- self._declare_fault(ConditionCode.FILESTORE_REJECTION)
-
- def _handle_transfer_completion(self):
- self._notice_of_completion()
- if (
- self.transmission_mode == TransmissionMode.UNACKNOWLEDGED
- and self._params.closure_requested
- ) or self.transmission_mode == TransmissionMode.ACKNOWLEDGED:
- self.states.step = TransactionStep.SENDING_FINISHED_PDU
- else:
- self.reset()
-
- def _lost_segment_handling(self, offset: int, data_len: int):
- """Lost segment detection: 4.6.4.3.1 a) and b) are covered by this code. c) is covered
- by dedicated code which is run when the EOF PDU is handled."""
- if offset > self._params.acked_params.last_end_offset:
- lost_segment = (self._params.acked_params.last_end_offset, offset)
- self._params.acked_params.lost_seg_tracker.add_lost_segment(
- (self._params.acked_params.last_end_offset, offset)
- )
- assert self._params.remote_cfg is not None
- if self._params.remote_cfg.immediate_nak_mode:
- self._add_packet_to_be_sent(
- NakPdu(
- self._params.pdu_conf,
- 0,
- offset + data_len,
- segment_requests=[lost_segment],
- )
- )
- if offset >= self._params.acked_params.last_end_offset:
- self._params.acked_params.last_start_offset = offset
- self._params.acked_params.last_end_offset = offset + data_len
- if offset + data_len <= self._params.acked_params.last_start_offset:
- # Might be a re-requested FD PDU.
- self._params.acked_params.lost_seg_tracker.remove_lost_segment(
- (offset, offset + data_len)
- )
-
- def _deferred_lost_segment_handling(self):
- if not self._params.acked_params.deferred_lost_segment_detection_active:
- return
- assert self._params.remote_cfg is not None
- assert self._params.fp.file_size_eof is not None
- if (
- self._params.acked_params.lost_seg_tracker.num_lost_segments == 0
- and not self._params.acked_params.metadata_missing
- ):
- # We are done and have received everything.
- self._checksum_verify()
- self.states.step = TransactionStep.TRANSFER_COMPLETION
- self._params.acked_params.deferred_lost_segment_detection_active = False
- return
- first_nak_issuance = False
- # This is the case if this is the first issuance of NAK PDUs
- # A timer needs to be instantiated, but we do not increment the activity counter yet.
- if self._params.acked_params.procedure_timer is None:
- self._params.acked_params.procedure_timer = Countdown.from_seconds(
- self._params.remote_cfg.nak_timer_interval_seconds
- )
- first_nak_issuance = True
- elif self._params.acked_params.procedure_timer.busy():
- # There were or there was a previous NAK sequence(s). Wait for timeout before issuing
- # a new NAK sequence.
- return
- if (
- not first_nak_issuance
- and self._params.acked_params.nak_activity_counter + 1
- == self._params.remote_cfg.nak_timer_expiration_limit
- ):
- self._declare_fault(ConditionCode.NAK_LIMIT_REACHED)
- return
- # This is not the first NAK issuance and the timer expired.
- max_segments_in_one_pdu = get_max_seg_reqs_for_max_packet_size_and_pdu_cfg(
- self._params.remote_cfg.max_packet_len, self._params.pdu_conf
- )
- next_segment_reqs = []
- if self._params.acked_params.metadata_missing:
- next_segment_reqs.append((0, 0))
- for (
- start,
- end,
- ) in self._params.acked_params.lost_seg_tracker.lost_segments.items():
- next_segment_reqs.append((start, end))
- if len(next_segment_reqs) == max_segments_in_one_pdu:
- self._add_packet_to_be_sent(
- NakPdu(
- self._params.pdu_conf,
- 0,
- self._params.fp.file_size_eof,
- next_segment_reqs,
- )
- )
- next_segment_reqs = []
- if len(next_segment_reqs) > 0:
- self._add_packet_to_be_sent(
- NakPdu(
- self._params.pdu_conf,
- 0,
- self._params.fp.file_size_eof,
- next_segment_reqs,
- )
- )
- if not first_nak_issuance:
- self._params.acked_params.nak_activity_counter += 1
- self._params.acked_params.procedure_timer.reset()
-
- def _handle_eof_pdu(self, eof_pdu: EofPdu):
- """Returns whether to exit the FSM prematurely."""
- self._params.fp.crc32 = eof_pdu.file_checksum
- self._params.fp.file_size_eof = eof_pdu.file_size
- self._params.fp.condition_code_eof = eof_pdu.condition_code
- if self.cfg.indication_cfg.eof_recv_indication_required:
- assert self._params.transaction_id is not None
- self.user.eof_recv_indication(self._params.transaction_id)
- if eof_pdu.condition_code == ConditionCode.NO_ERROR:
- regular_completion = self._handle_no_error_eof()
- if not regular_completion:
- return
- else:
- # This is an EOF (Cancel), perform Cancel Response Procedures according to chapter
- # 4.6.6 of the standard.
- self._trigger_notice_of_completion_canceled(eof_pdu.condition_code)
- # Store this as progress for the checksum calculation.
- self._params.fp.progress = self._params.fp.file_size_eof
- self._params.finished_params.delivery_code = DeliveryCode.DATA_INCOMPLETE
- self._file_transfer_complete_transition()
- return False
-
- def _handle_no_error_eof(self) -> bool:
- """Returns whether the transfer can be completed regularly."""
- # CFDP 4.6.1.2.9: Declare file size error if progress exceeds file size
- if self._params.fp.progress > self._params.fp.file_size_eof: # type: ignore
- if (
- self._declare_fault(ConditionCode.FILE_SIZE_ERROR)
- != FaultHandlerCode.IGNORE_ERROR
- ):
- return False
- elif (
- self._params.fp.progress < self._params.fp.file_size_eof # type: ignore
- ) and self.transmission_mode == TransmissionMode.ACKNOWLEDGED:
- # CFDP 4.6.4.3.1: The end offset of the last received file segment and the file
- # size as stated in the EOF PDU is not the same, so we need to add that segment to
- # the lost segments for the deferred lost segment detection procedure.
- self._params.acked_params.lost_seg_tracker.add_lost_segment(
- (self._params.fp.progress, self._params.fp.file_size_eof) # type: ignore
- )
- if self._params.fp.file_size_eof != self._params.fp.file_size:
- # Can or should this ever happen for a No Error EOF? Treat this like a non-fatal
- # error for now..
- _LOGGER.warning(
- "missmatch of EOF file size and Metadata File Size for success EOF"
- )
- if (
- self.transmission_mode == TransmissionMode.UNACKNOWLEDGED
- and not self._checksum_verify()
- ):
- if (
- self._declare_fault(ConditionCode.FILE_CHECKSUM_FAILURE)
- != FaultHandlerCode.IGNORE_ERROR
- ):
- return False
- self._start_check_limit_handling()
- return False
- return True
-
- def _start_deferred_lost_segment_handling(self):
- if self._params.acked_params.metadata_missing:
- self.states.step = TransactionStep.WAITING_FOR_METADATA
- else:
- self.states.step = TransactionStep.WAITING_FOR_MISSING_DATA
- self._params.acked_params.deferred_lost_segment_detection_active = True
- self._params.acked_params.lost_seg_tracker.coalesce_lost_segments()
- self._params.acked_params.last_start_offset = self._params.fp.file_size_eof # type: ignore
- self._params.acked_params.last_end_offset = self._params.fp.file_size_eof # type: ignore
- self._deferred_lost_segment_handling()
-
- def _prepare_eof_ack_packet(self):
- assert self._params.fp.condition_code_eof is not None
- ack_pdu = AckPdu(
- self._params.pdu_conf,
- DirectiveType.EOF_PDU,
- self._params.fp.condition_code_eof,
- TransactionStatus.ACTIVE,
- )
- self._add_packet_to_be_sent(ack_pdu)
-
- def _checksum_verify(self) -> bool:
- file_delivery_complete = False
- if (
- self._cksum_verif_helper.checksum_type == ChecksumType.NULL_CHECKSUM
- or self._params.fp.metadata_only
- ):
- file_delivery_complete = True
- else:
- crc32 = self._cksum_verif_helper.calc_for_file(
- self._params.fp.file_name, self._params.fp.progress
- )
- if crc32 == self._params.fp.crc32:
- file_delivery_complete = True
- else:
- self._declare_fault(ConditionCode.FILE_CHECKSUM_FAILURE)
- if file_delivery_complete:
- self._params.finished_params.delivery_code = DeliveryCode.DATA_COMPLETE
- self._params.finished_params.condition_code = ConditionCode.NO_ERROR
- return file_delivery_complete
-
- def _file_transfer_complete_transition(self):
- if self.transmission_mode == TransmissionMode.UNACKNOWLEDGED:
- self.states.step = TransactionStep.TRANSFER_COMPLETION
- elif self.transmission_mode == TransmissionMode.ACKNOWLEDGED:
- self._prepare_eof_ack_packet()
- self.states.step = TransactionStep.SENDING_EOF_ACK_PDU
-
- def _trigger_notice_of_completion_canceled(self, condition_code: ConditionCode):
- self._params.completion_disposition = CompletionDisposition.CANCELLED
- self._params.finished_params.condition_code = condition_code
-
- def _start_check_limit_handling(self):
- self.states.step = TransactionStep.RECV_FILE_DATA_WITH_CHECK_LIMIT_HANDLING
- assert self._params.remote_cfg is not None
- self._params.check_timer = self.check_timer_provider.provide_check_timer(
- self.cfg.local_entity_id,
- self._params.remote_cfg.entity_id,
- EntityType.RECEIVING,
- )
- self._params.current_check_count = 0
-
- def _notice_of_completion(self):
- if self._params.completion_disposition == CompletionDisposition.COMPLETED:
- # TODO: Execute any filestore requests
- pass
- elif self._params.completion_disposition == CompletionDisposition.CANCELLED:
- assert self._params.remote_cfg is not None
- if (
- self._params.remote_cfg.disposition_on_cancellation
- and self._params.finished_params.delivery_code
- == DeliveryCode.DATA_INCOMPLETE
- ):
- self.user.vfs.delete_file(self._params.fp.file_name)
- self._params.finished_params.file_status = (
- FileStatus.DISCARDED_DELIBERATELY
- )
- if self.cfg.indication_cfg.transaction_finished_indication_required:
- finished_indic_params = TransactionFinishedParams(
- transaction_id=self._params.transaction_id, # type: ignore
- finished_params=self._params.finished_params,
- status_report=None,
- )
- self.user.transaction_finished_indication(finished_indic_params)
-
- def _prepare_finished_pdu(self):
- if self.states.packets_ready:
- raise UnretrievedPdusToBeSent()
- finished_pdu = FinishedPdu(
- params=self._params.finished_params,
- # The configuration was cached when the first metadata arrived
- pdu_conf=self._params.pdu_conf,
- )
- self._add_packet_to_be_sent(finished_pdu)
-
- def _start_positive_ack_procedure(self):
- assert self._params.remote_cfg is not None
- self._params.positive_ack_params.ack_timer = Countdown.from_seconds(
- self._params.remote_cfg.positive_ack_timer_interval_seconds
- )
- self._params.positive_ack_params.ack_counter = 0
-
- def _add_packet_to_be_sent(self, packet: GenericPduPacket):
- self._pdus_to_be_sent.append(PduHolder(packet))
- self.states._num_packets_ready += 1
-
- def _check_limit_handling(self):
- assert self._params.check_timer is not None
- assert self._params.remote_cfg is not None
- if self._params.check_timer.timed_out():
- if self._checksum_verify():
- self._file_transfer_complete_transition()
- return
- if (
- self._params.current_check_count + 1
- >= self._params.remote_cfg.check_limit
- ):
- self._declare_fault(ConditionCode.CHECK_LIMIT_REACHED)
- else:
- self._params.current_check_count += 1
- self._params.check_timer.reset()
-
- def _declare_fault(self, cond: ConditionCode) -> FaultHandlerCode:
- fh = self.cfg.default_fault_handlers.get_fault_handler(cond)
- transaction_id = self._params.transaction_id
- progress = self._params.fp.progress
- assert transaction_id is not None
- if fh is None:
- raise ValueError(f"invalid condition code {cond!r} for fault declaration")
- if fh == FaultHandlerCode.NOTICE_OF_CANCELLATION:
- self._notice_of_cancellation(cond)
- elif fh == FaultHandlerCode.NOTICE_OF_SUSPENSION:
- self._notice_of_suspension()
- elif fh == FaultHandlerCode.ABANDON_TRANSACTION:
- self._abandon_transaction()
- self.cfg.default_fault_handlers.report_fault(transaction_id, cond, progress)
- return fh
-
- def _notice_of_cancellation(self, condition_code: ConditionCode):
- self.states.step = TransactionStep.TRANSFER_COMPLETION
- self._params.finished_params.condition_code = condition_code
- self._params.completion_disposition = CompletionDisposition.CANCELLED
-
- def _notice_of_suspension(self):
- # TODO: Implement
- pass
-
- def _abandon_transaction(self):
- # I guess an abandoned transaction just stops whatever it is doing.. The implementation
- # for this is quite easy.
- self.reset()
-
- @deprecated(
- version="6.0.0rc1",
- reason="Use insert_packet instead",
- )
- def pass_packet(self, packet: GenericPduPacket):
- self.insert_packet(packet)
diff --git a/tmtccmd/cfdp/handler/source.py b/tmtccmd/cfdp/handler/source.py
deleted file mode 100644
index ac03d2d2..00000000
--- a/tmtccmd/cfdp/handler/source.py
+++ /dev/null
@@ -1,1026 +0,0 @@
-from __future__ import annotations
-
-import enum
-import logging
-from pathlib import Path
-from collections import deque
-from dataclasses import dataclass
-from typing import Deque, Optional, Tuple
-from deprecated.sphinx import deprecated
-from spacepackets.cfdp import (
- CrcFlag,
- GenericPduPacket,
- LargeFileFlag,
- PduType,
- TransmissionMode,
- NULL_CHECKSUM_U32,
- ConditionCode,
- Direction,
- PduConfig,
- ChecksumType,
- FaultHandlerCode,
- TransactionId,
-)
-from spacepackets.cfdp.tlv import ProxyMessageType
-from spacepackets.cfdp.pdu import (
- DeliveryCode,
- FileStatus,
- PduHolder,
- EofPdu,
- FileDataPdu,
- MetadataPdu,
- AckPdu,
- MetadataParams,
- DirectiveType,
- AbstractFileDirectiveBase,
- TransactionStatus,
-)
-from spacepackets.cfdp.pdu.file_data import (
- FileDataParams,
- get_max_file_seg_len_for_max_packet_len_and_pdu_cfg,
-)
-from spacepackets.cfdp.pdu.finished import FinishedParams
-from spacepackets.util import UnsignedByteField, ByteFieldGenerator
-
-from tmtccmd.cfdp import (
- LocalEntityCfg,
- CfdpUserBase,
- RemoteEntityCfg,
-)
-from tmtccmd.cfdp.defs import CfdpState
-from tmtccmd.cfdp.exceptions import (
- InvalidNakPdu,
- InvalidTransactionSeqNum,
- UnretrievedPdusToBeSent,
- SourceFileDoesNotExist,
- InvalidPduDirection,
- InvalidSourceId,
- InvalidDestinationId,
- NoRemoteEntityCfgFound,
- FsmNotCalledAfterPacketInsertion,
- PduIgnoredForSource,
- PduIgnoredForSourceReason,
- InvalidPduForSourceHandler,
-)
-from tmtccmd.cfdp.handler.common import _PositiveAckProcedureParams
-from tmtccmd.cfdp.handler.crc import CrcHelper
-from tmtccmd.cfdp.handler.defs import (
- _FileParamsBase,
-)
-from tmtccmd.cfdp.mib import CheckTimerProvider, EntityType, RemoteEntityCfgTable
-from tmtccmd.cfdp.request import PutRequest
-from tmtccmd.cfdp.user import TransactionFinishedParams, TransactionParams
-from tmtccmd.util import ProvidesSeqCount
-from tmtccmd.util.countdown import Countdown
-
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class TransactionStep(enum.Enum):
- IDLE = 0
- TRANSACTION_START = 1
- # The following three are used for the Copy File Procedure
- SENDING_METADATA = 3
- SENDING_FILE_DATA = 4
- RETRANSMITTING = 5
- """Re-transmitting missing packets in acknowledged mode."""
- SENDING_EOF = 6
- WAITING_FOR_EOF_ACK = 7
- WAITING_FOR_FINISHED = 8
- SENDING_ACK_OF_FINISHED = 9
- NOTICE_OF_COMPLETION = 10
-
-
-@dataclass
-class _SourceFileParams(_FileParamsBase):
- no_eof: bool = False
-
- @classmethod
- def empty(cls) -> _SourceFileParams:
- return cls(
- progress=0,
- segment_len=0,
- crc32=bytes(),
- file_size=0,
- no_eof=False,
- metadata_only=False,
- )
-
- def reset(self):
- super().reset()
-
-
-@dataclass
-class SourceStateWrapper:
- state: CfdpState = CfdpState.IDLE
- step: TransactionStep = TransactionStep.IDLE
- _num_packets_ready: int = 0
-
- @property
- def num_packets_ready(self) -> int:
- return self._num_packets_ready
-
- @property
- def packets_ready(self) -> bool:
- return self.num_packets_ready > 0
-
-
-class _AckedModeParams:
- def __init__(self) -> None:
- self.step_before_retransmission: Optional[TransactionStep] = None
- self.segment_reqs_to_handle: Optional[Tuple[int, int]] = None
- self.segment_req_index: int = 0
-
-
-class _TransferFieldWrapper:
- def __init__(self, local_entity_id: UnsignedByteField):
- self.transaction_id: Optional[TransactionId] = None
- self.check_timer: Optional[Countdown] = None
- self.positive_ack_params: _PositiveAckProcedureParams = (
- _PositiveAckProcedureParams()
- )
- self.cond_code_eof: Optional[ConditionCode] = None
- self.ack_params: _AckedModeParams = _AckedModeParams()
- self.fp: _SourceFileParams = _SourceFileParams.empty()
- self.finished_params: Optional[FinishedParams] = None
- self.remote_cfg: Optional[RemoteEntityCfg] = None
- self.closure_requested: bool = False
- self.pdu_conf = PduConfig.empty()
- self.pdu_conf.source_entity_id = local_entity_id
-
- @property
- def source_id(self) -> UnsignedByteField:
- return self.pdu_conf.source_entity_id
-
- @source_id.setter
- def source_id(self, source_id: UnsignedByteField):
- self.pdu_conf.source_entity_id = source_id
-
- @property
- def positve_ack_counter(self) -> int:
- return self.positive_ack_params.ack_counter
-
- @property
- def dest_id(self):
- return self.pdu_conf.dest_entity_id
-
- @dest_id.setter
- def dest_id(self, dest_id: UnsignedByteField):
- self.pdu_conf.dest_entity_id = dest_id
-
- @property
- def transmission_mode(self) -> TransmissionMode:
- return self.pdu_conf.trans_mode
-
- @transmission_mode.setter
- def transmission_mode(self, trans_mode: TransmissionMode):
- self.pdu_conf.trans_mode = trans_mode
-
- @property
- def transaction_seq_num(self) -> UnsignedByteField:
- return self.pdu_conf.transaction_seq_num
-
- @transaction_seq_num.setter
- def transaction_seq_num(self, seq_num: UnsignedByteField):
- self.pdu_conf.transaction_seq_num = seq_num
-
- def reset(self):
- self.fp.reset()
- self.remote_cfg = None
- self.transaction_id = None
- self.check_timer = None
- self.cond_code_eof = None
- self.closure_requested = False
- self.pdu_conf = PduConfig.empty()
- self.finished_params = None
- self.positive_ack_params = _PositiveAckProcedureParams()
-
-
-class FsmResult:
- def __init__(self, states: SourceStateWrapper):
- self.states = states
-
-
-class SourceHandler:
- """This is the primary CFDP source handler. It models the CFDP source entity, which is
- primarily responsible for handling put requests to send files to another CFDP destination
- entity.
-
- As such, it contains a state machine to perform all operations necessary to perform a
- source-to-destination file transfer. This class does not send the CFDP PDU packets directly
- to allow for greater flexibility. For example, a user might want to wrap the CFDP packet
- entities into a CCSDS space packet or into a special frame type. The user is responsible for
- sending the packets and confirming that they are sent successfully. The handler can handle
- both unacknowledged (class 1) and acknowledged (class 2) file tranfers.
-
- The following core functions are the primary interface:
-
- 1. :py:meth:`put_request` : Can be used to start transactions, most notably to start
- and perform a Copy File procedure to send a file or to send a Proxy Put Request to request
- a file.
- 2. :py:meth:`insert_packet` : Can be used to insert packets into the source
- handler. Please note that the source handler can also process Finished, Keep Alive and
- NAK PDUs in addition to ACK PDUs where the acknowledged PDU is the EOF PDU.
- 3. :py:meth:`state_machine` : This state machine generates the necessary CFDP PDUs necessary
- to perform a CFDP file transfer. The PDUs are returned in a special wrapper result type.
- 4. :py:meth:`get_next_packet` : Retrieve the next packet which should be sent to the remote
- entity of a file copy operation. This function might also yield multiple packets on
- subsequent calls.
-
- A put request will only be accepted if the handler is in the idle state. Furthermore,
- packet insertion is not allowed until all packets to send were retrieved after a state machine
- call.
-
- This handler also does not support concurrency out of the box. Instead, if concurrent handling
- is required, it is recommended to create a new handler and run those inside a thread pool,
- or move the newly created handler to a new thread."""
-
- def __init__(
- self,
- cfg: LocalEntityCfg,
- user: CfdpUserBase,
- remote_cfg_table: RemoteEntityCfgTable,
- check_timer_provider: CheckTimerProvider,
- seq_num_provider: ProvidesSeqCount,
- ):
- self.states = SourceStateWrapper()
- self.cfg = cfg
- self.user = user
- self.remote_cfg_table = remote_cfg_table
- self.seq_num_provider = seq_num_provider
- self.check_timer_provider = check_timer_provider
- self._params = _TransferFieldWrapper(cfg.local_entity_id)
- self._crc_helper = CrcHelper(ChecksumType.NULL_CHECKSUM, self.user.vfs)
- self._put_req: Optional[PutRequest] = None
- self._inserted_pdu = PduHolder(None)
- self._pdus_to_be_sent: Deque[PduHolder] = deque()
-
- @property
- def entity_id(self) -> UnsignedByteField:
- return self.cfg.local_entity_id
-
- @entity_id.setter
- def entity_id(self, entity_id: UnsignedByteField):
- self.cfg.local_entity_id = entity_id
- self._params.source_id = entity_id
-
- @property
- def transaction_seq_num(self) -> UnsignedByteField:
- return self.pdu_conf.transaction_seq_num
-
- @property
- def pdu_conf(self) -> PduConfig:
- return self._params.pdu_conf
-
- @property
- def positive_ack_counter(self) -> int:
- return self._params.positve_ack_counter
-
- @property
- def transmission_mode(self) -> Optional[TransmissionMode]:
- if self.state == CfdpState.IDLE:
- return None
- return self._params.transmission_mode
-
- @property
- def state(self) -> CfdpState:
- return self.states.state
-
- @property
- def step(self) -> TransactionStep:
- return self.states.step
-
- @property
- def packets_ready(self) -> bool:
- return self.states.packets_ready
-
- @property
- def num_packets_ready(self) -> int:
- return self.states.num_packets_ready
-
- def put_request(self, request: PutRequest):
- """You can call this function to pass a put request to the source handler, which is
- also used to start a file copy operation. As such, this function models the Put.request
- CFDP primtiive.
-
- Please note that the source handler can also process one put request at a time.
- The caller is responsible of creating a new source handler, one handler can only handle
- one file copy request at a time.
-
-
- Raises
- --------
-
- ValueError
- Invalid transmission mode detected.
- NoRemoteEntityCfgFound
- No remote configuration found for destination ID specified in the Put Request.
- SourceFileDoesNotExist
- File specified for Put Request does not exist.
-
- Returns
- --------
-
- False if the handler is busy. True if the handling of the request was successfull.
- """
- if self.states.state != CfdpState.IDLE:
- _LOGGER.debug("CFDP source handler is busy, can't process put request")
- return False
- self._put_req = request
- if self._put_req.source_file is not None:
- assert isinstance(self._put_req.source_file, Path)
- if not self.user.vfs.file_exists(self._put_req.source_file):
- raise SourceFileDoesNotExist(self._put_req.source_file)
- if self._put_req.dest_file is not None:
- assert isinstance(self._put_req.dest_file, Path)
- self._params.remote_cfg = self.remote_cfg_table.get_cfg(request.destination_id)
- if self._params.remote_cfg is None:
- raise NoRemoteEntityCfgFound(entity_id=request.destination_id)
- self._params.dest_id = request.destination_id
- self.states._num_packets_ready = 0
- self.states.state = CfdpState.BUSY
- self._setup_transmission_mode()
- if self._params.transmission_mode == TransmissionMode.UNACKNOWLEDGED:
- _LOGGER.debug("Starting Put Request handling in NAK mode")
- elif self._params.transmission_mode == TransmissionMode.ACKNOWLEDGED:
- _LOGGER.debug("Starting Put Request handling in ACK mode")
- else:
- raise ValueError(
- f"Invalid transmission mode {self._params.transmission_mode} passed"
- )
- return True
-
- def cancel_request(self, transaction_id: TransactionId) -> bool:
- """This function models the Cancel.request CFDP primtive and is the recommended way
- to cancel a transaction. It will cause a Notice Of Cancellation at this entity.
- Please note that the state machine might still be active because a canceled transfer
- might still require some packets to be sent to the remote receiver entity.
-
- Returns
- --------
- True
- Current transfer was cancelled
- False
- The state machine is in the IDLE state or there is a transaction ID missmatch.
- """
- if self.states.step == CfdpState.IDLE:
- return False
- if self.states.packets_ready:
- raise UnretrievedPdusToBeSent()
- if (
- self._params.transaction_id is not None
- and transaction_id == self._params.transaction_id
- ):
- self._declare_fault(ConditionCode.CANCEL_REQUEST_RECEIVED)
- return True
- return False
-
- def insert_packet(self, packet: AbstractFileDirectiveBase):
- """Pass PDU file directives going towards the file sender to the CFDP source handler.
- Please note that only one packet can be inserted into the source handler at a given time.
- The packet is then handled by calling the :py:meth:`state_machine` and will be
- cleared after the packet was successfully handled, allowing insertion of new packets.
-
- Raises
- -------
- InvalidPduDirection
- PDU direction field wrong.
- FsmNotCalledAfterPacketInsertion
- :py:meth:`state_machine` was not called after packet insertion.
- InvalidPduForSourceHandler
- Invalid PDU file directive type.
- PduIgnoredForSource
- The specified PDU can not be handled in the current state.
- NoRemoteEntityCfgFound
- No remote configuration found for specified destination entity.
- InvalidSourceId
- Source ID not identical to local entity ID.
- InvalidDestinationId
- Destination ID was found, but there is a mismatch between the packet destination ID
- and the remote configuration entity ID.
-
- """
- if self._inserted_pdu.pdu is not None:
- raise FsmNotCalledAfterPacketInsertion()
- if packet.pdu_header.direction != Direction.TOWARDS_SENDER:
- raise InvalidPduDirection(
- Direction.TOWARDS_SENDER, packet.pdu_header.direction
- )
- if packet.source_entity_id.value != self.entity_id.value:
- raise InvalidSourceId(self.entity_id, packet.source_entity_id)
- # TODO: This can happen if a packet is received for which no transaction was started..
- # A better exception might be worth a thought..
- if self._params.remote_cfg is None:
- raise NoRemoteEntityCfgFound(entity_id=packet.dest_entity_id)
- if packet.dest_entity_id.value != self._params.remote_cfg.entity_id.value:
- raise InvalidDestinationId(
- self._params.remote_cfg.entity_id, packet.dest_entity_id
- )
-
- if packet.transaction_seq_num.value != self._params.transaction_seq_num.value:
- raise InvalidTransactionSeqNum(
- self._params.transaction_seq_num, packet.transaction_seq_num
- )
- if packet.directive_type in [
- DirectiveType.METADATA_PDU,
- DirectiveType.EOF_PDU,
- DirectiveType.PROMPT_PDU,
- ]:
- raise InvalidPduForSourceHandler(packet)
- if self._params.transmission_mode == TransmissionMode.UNACKNOWLEDGED and (
- packet.directive_type == DirectiveType.KEEP_ALIVE_PDU
- or packet.directive_type == DirectiveType.NAK_PDU
- ):
- raise PduIgnoredForSource(
- reason=PduIgnoredForSourceReason.ACK_MODE_PACKET_INVALID_MODE,
- ignored_packet=packet,
- )
- if packet.directive_type != DirectiveType.NAK_PDU:
- if (
- self.states.step == TransactionStep.WAITING_FOR_EOF_ACK
- and packet.directive_type != DirectiveType.ACK_PDU
- ):
- raise PduIgnoredForSource(
- reason=PduIgnoredForSourceReason.NOT_WAITING_FOR_ACK,
- ignored_packet=packet,
- )
- if (
- self.states.step == TransactionStep.WAITING_FOR_FINISHED
- and packet.directive_type != DirectiveType.FINISHED_PDU
- ):
- raise PduIgnoredForSource(
- reason=PduIgnoredForSourceReason.NOT_WAITING_FOR_FINISHED_PDU,
- ignored_packet=packet,
- )
- self._inserted_pdu.pdu = packet
-
- def get_next_packet(self) -> Optional[PduHolder]:
- """Retrieve the next packet which should be sent to the remote CFDP destination entity."""
- if len(self._pdus_to_be_sent) == 0:
- return None
- self.states._num_packets_ready -= 1
- return self._pdus_to_be_sent.popleft()
-
- def state_machine(self) -> FsmResult:
- """This is the primary state machine which performs the CFDP procedures like CRC calculation
- and PDU generation. The packets generated by this finite-state machine (FSM) need to be
- sent by the user and can be retrieved using the
- :py:class:`spacepackets.cfdp.pdu.helper.PduHolder` class contained in the
- returned :py:class:`tmtccmd.cfdp.handler.source.FsmResult`. After the packet was sent,
- the calling code has to call :py:meth:`confirm_packet_sent` and :py:meth:`advance_fsm`
- for the next state machine call do perform the next transaction step.
- There is also the helper method :py:meth:`confirm_packet_sent_advance_fsm` available
- to perform both steps.
-
- Raises
- --------
- UnretrievedPdusToBeSent
- There are still PDUs which need to be sent before calling the FSM again.
- ChecksumNotImplemented
- Right now, only a subset of the checksums specified for the CFDP standard are implemented.
- SourceFileDoesNotExist
- The source file for which a transaction was requested does not exist. This can happen
- if the file is deleted during a transaction.
- """
- if self.states.state == CfdpState.IDLE:
- return FsmResult(self.states)
- self._fsm_non_idle()
- return FsmResult(self.states)
-
- @property
- def transaction_id(self) -> Optional[TransactionId]:
- return self._params.transaction_id
-
- def reset(self):
- """This function is public to allow completely resetting the handler, but it is explicitely
- discouraged to do this. CFDP generally has mechanism to detect issues and errors on itself.
- """
- self.states.step = TransactionStep.IDLE
- self.states.state = CfdpState.IDLE
- self._pdus_to_be_sent.clear()
- self._params.reset()
-
- def _fsm_non_idle(self):
- self._fsm_advancement_after_packets_were_sent()
- if self._put_req is None:
- return
- if self.states.step == TransactionStep.IDLE:
- self.states.step = TransactionStep.TRANSACTION_START
- if self.states.step == TransactionStep.TRANSACTION_START:
- self._transaction_start()
- self.states.step = TransactionStep.SENDING_METADATA
- if self.states.step == TransactionStep.SENDING_METADATA:
- self._prepare_metadata_pdu()
- return
- if self.states.step == TransactionStep.SENDING_FILE_DATA:
- if self._sending_file_data_fsm():
- return
- if self.states.step == TransactionStep.SENDING_EOF:
- self._prepare_eof_pdu(
- self._checksum_calculation(self._params.fp.file_size),
- )
- return
- if self.states.step == TransactionStep.WAITING_FOR_EOF_ACK:
- self._handle_waiting_for_ack()
- if self.states.step == TransactionStep.WAITING_FOR_FINISHED:
- self._handle_wait_for_finish()
- if self.states.step == TransactionStep.NOTICE_OF_COMPLETION:
- self._notice_of_completion()
-
- def _transaction_start(self):
- file_size = 0
- originating_transaction_id = self._check_for_originating_id()
- self._prepare_file_params()
- self._prepare_pdu_conf(file_size)
- self._get_next_transfer_seq_num()
- self._calculate_max_file_seg_len()
- self._params.transaction_id = TransactionId(
- source_entity_id=self.cfg.local_entity_id,
- transaction_seq_num=self.transaction_seq_num,
- )
- self.user.transaction_indication(
- TransactionParams(self._params.transaction_id, originating_transaction_id)
- )
-
- def _check_for_originating_id(self) -> Optional[TransactionId]:
- """This function only returns an originating ID for if not proxy put response is
- contained in the message to user list. This special logic is in place to avoid permanent
- loop which would occur when the user uses the orignating ID to register active proxy put
- request, and this ID would also be generated for proxy put responses."""
- contains_proxy_put_response = False
- contains_originating_id = False
- originating_id = None
- if self._put_req.msgs_to_user is None:
- return None
- for msgs_to_user in self._put_req.msgs_to_user:
- if msgs_to_user.is_reserved_cfdp_message():
- reserved_cfdp_msg = msgs_to_user.to_reserved_msg_tlv()
- if reserved_cfdp_msg.is_originating_transaction_id():
- contains_originating_id = True
- originating_id = reserved_cfdp_msg.get_originating_transaction_id()
- if (
- reserved_cfdp_msg.is_cfdp_proxy_operation()
- and reserved_cfdp_msg.get_cfdp_proxy_message_type()
- == ProxyMessageType.PUT_RESPONSE
- ):
- contains_proxy_put_response = True
- if not contains_proxy_put_response and contains_originating_id:
- return originating_id
- return None
-
- def _prepare_file_params(self):
- assert self._put_req is not None
- if self._put_req.metadata_only:
- self._params.fp.metadata_only = True
- self._params.fp.no_eof = True
- else:
- assert self._put_req.source_file is not None
- if not self._put_req.source_file.exists():
- # TODO: Handle this exception in the handler, reset CFDP state machine
- raise SourceFileDoesNotExist(self._put_req.source_file)
- file_size = self._put_req.source_file.stat().st_size
- if file_size == 0:
- self._params.fp.metadata_only = True
- else:
- self._params.fp.file_size = file_size
-
- def _prepare_pdu_conf(self, file_size: int):
- # Please note that the transmission mode and closure requested field were set in
- # a previous step.
- assert self._put_req is not None
- assert self._params.remote_cfg is not None
- if file_size > pow(2, 32) - 1:
- self._params.pdu_conf.file_flag = LargeFileFlag.LARGE
- else:
- self._params.pdu_conf.file_flag = LargeFileFlag.NORMAL
- if self._put_req.seg_ctrl is not None:
- self._params.pdu_conf.seg_ctrl = self._put_req.seg_ctrl
- # Both the source entity and destination entity ID field must have the same size.
- # We use the larger of either the Put Request destination ID or the local entity ID
- # as the size for the new entity IDs.
- larger_entity_width = max(
- self.cfg.local_entity_id.byte_len, self._put_req.destination_id.byte_len
- )
- if larger_entity_width != self.cfg.local_entity_id.byte_len:
- self._params.pdu_conf.source_entity_id = UnsignedByteField(
- self.cfg.local_entity_id.value, larger_entity_width
- )
- else:
- self._params.pdu_conf.source_entity_id = self.cfg.local_entity_id
-
- if larger_entity_width != self._put_req.destination_id.byte_len:
- self._params.pdu_conf.dest_entity_id = UnsignedByteField(
- self._put_req.destination_id.value, larger_entity_width
- )
- else:
- self._params.pdu_conf.dest_entity_id = self._put_req.destination_id
-
- self._params.pdu_conf.crc_flag = CrcFlag(
- self._params.remote_cfg.crc_on_transmission
- )
- self._params.pdu_conf.direction = Direction.TOWARDS_RECEIVER
-
- def _calculate_max_file_seg_len(self):
- assert self._params.remote_cfg is not None
- derived_max_seg_len = get_max_file_seg_len_for_max_packet_len_and_pdu_cfg(
- self._params.pdu_conf, self._params.remote_cfg.max_packet_len
- )
- self._params.fp.segment_len = derived_max_seg_len
- if (
- self._params.remote_cfg.max_file_segment_len is not None
- and self._params.remote_cfg.max_file_segment_len < derived_max_seg_len
- ):
- self._params.fp.segment_len = self._params.remote_cfg.max_file_segment_len
-
- def _prepare_metadata_pdu(self):
- assert self._put_req is not None
- options = []
- if self._put_req.metadata_only:
- params = MetadataParams(
- closure_requested=self._params.closure_requested,
- checksum_type=self._crc_helper.checksum_type,
- file_size=0,
- dest_file_name=None,
- source_file_name=None,
- )
- else:
- # Funny name.
- params = self._prepare_metadata_base_params_with_metadata()
- if self._put_req.fs_requests is not None:
- for fs_request in self._put_req.fs_requests:
- options.append(fs_request)
- if self._put_req.fault_handler_overrides is not None:
- for fh_override in self._put_req.fault_handler_overrides:
- options.append(fh_override)
- if self._put_req.flow_label_tlv is not None:
- options.append(self._put_req.flow_label_tlv)
- if self._put_req.msgs_to_user is not None:
- for msg_to_user in self._put_req.msgs_to_user:
- options.append(msg_to_user)
- self._add_packet_to_be_sent(
- MetadataPdu(pdu_conf=self._params.pdu_conf, params=params, options=options)
- )
-
- def _prepare_metadata_base_params_with_metadata(self) -> MetadataParams:
- return MetadataParams(
- dest_file_name=self._put_req.dest_file.as_posix(), # type: ignore
- source_file_name=self._put_req.source_file.as_posix(), # type: ignore
- checksum_type=self._crc_helper.checksum_type,
- closure_requested=self._params.closure_requested,
- file_size=self._params.fp.file_size,
- )
-
- def _sending_file_data_fsm(self) -> bool:
- # This function returns whether the internal state was advanced or not.
- # During the PDU send phase, handle the re-transmission of missing files in
- # acknowledged mode.
- if self.transmission_mode == TransmissionMode.ACKNOWLEDGED:
- if self.__handle_retransmission():
- return True
- if self._prepare_progressing_file_data_pdu():
- return True
- if self._params.fp.no_eof:
- # Special case: Metadata Only.
- if self._params.closure_requested:
- self.states.step = TransactionStep.WAITING_FOR_FINISHED
- else:
- self.states.step = TransactionStep.NOTICE_OF_COMPLETION
- else:
- # Special case: Empty file.
- self._params.cond_code_eof = ConditionCode.NO_ERROR
- self.states.step = TransactionStep.SENDING_EOF
- return False
-
- def __handle_retransmission(self) -> bool:
- """Returns whether a packet was generated and re-transmission is active."""
- if self._inserted_pdu.pdu is None:
- return False
- if self._inserted_pdu.pdu_directive_type != DirectiveType.NAK_PDU:
- return False
- nak_pdu = self._inserted_pdu.to_nak_pdu()
- for segment_req in nak_pdu.segment_requests:
- self._handle_segment_req(segment_req)
- self._params.ack_params.step_before_retransmission = self.states.step
- self.states.step = TransactionStep.RETRANSMITTING
- self._inserted_pdu.pdu = None
- return True
-
- def _handle_segment_req(self, segment_req: Tuple[int, int]):
- # Special case: Metadata PDU is re-requested
- if segment_req[0] == 0 and segment_req[1] == 0:
- # Re-transmit the metadata PDU
- self._prepare_metadata_pdu()
- else:
- if segment_req[1] < segment_req[0]:
- raise InvalidNakPdu("end offset larger than start offset")
- elif segment_req[0] > self._params.fp.progress:
- raise InvalidNakPdu("start offset larger than current file progress")
-
- missing_chunk_len = segment_req[1] - segment_req[0]
- current_offset = segment_req[0]
- while missing_chunk_len > 0:
- chunk_size = min(missing_chunk_len, self._params.fp.segment_len)
- self._prepare_file_data_pdu(current_offset, chunk_size)
- current_offset += chunk_size
- missing_chunk_len -= chunk_size
-
- def _handle_waiting_for_ack(self):
- if self.transmission_mode == TransmissionMode.UNACKNOWLEDGED:
- _LOGGER.error(
- f"invalid ACK waiting function call for transmission mode "
- f"{self.transmission_mode!r}"
- )
- if self.__handle_retransmission():
- return
- if self._inserted_pdu.pdu is None or (
- self._inserted_pdu.pdu_type == PduType.FILE_DIRECTIVE
- and self._inserted_pdu.pdu_directive_type != DirectiveType.ACK_PDU
- ):
- self._handle_positive_ack_procedures()
- return
- ack_pdu = self._inserted_pdu.to_ack_pdu()
- if ack_pdu.directive_code_of_acked_pdu == DirectiveType.EOF_PDU:
- # TODO: Equality check required? I am not sure why the condition code is supplied
- # as part of the ACK packet.
- self.states.step = TransactionStep.WAITING_FOR_FINISHED
- else:
- _LOGGER.error(
- f"received ACK PDU with invalid acked directive code"
- f" {ack_pdu.directive_code_of_acked_pdu!r}"
- )
-
- self._inserted_pdu.pdu = None
-
- def _handle_positive_ack_procedures(self):
- """Positive ACK procedures according to chapter 4.7.1 of the CFDP standard."""
- assert self._params.positive_ack_params.ack_timer is not None
- assert self._params.remote_cfg is not None
- if self._params.positive_ack_params.ack_timer.timed_out():
- if (
- self._params.positive_ack_params.ack_counter + 1
- >= self._params.remote_cfg.positive_ack_timer_expiration_limit
- ):
- self._declare_fault(ConditionCode.POSITIVE_ACK_LIMIT_REACHED)
- return
- self._params.positive_ack_params.ack_timer.reset()
- self._params.positive_ack_params.ack_counter += 1
- self._prepare_eof_pdu(
- self._checksum_calculation(self._params.fp.file_size),
- )
-
- def _handle_wait_for_finish(self):
- if (
- self.transmission_mode == TransmissionMode.ACKNOWLEDGED
- and self.__handle_retransmission()
- ):
- return
- if (
- self._inserted_pdu.pdu is None
- or self._inserted_pdu.pdu_directive_type is None
- or self._inserted_pdu.pdu_directive_type != DirectiveType.FINISHED_PDU
- ):
- if self._params.check_timer is not None:
- if self._params.check_timer.timed_out():
- self._declare_fault(ConditionCode.CHECK_LIMIT_REACHED)
- return
- finished_pdu = self._inserted_pdu.to_finished_pdu()
- self._inserted_pdu.pdu = None
- self._params.finished_params = finished_pdu.finished_params
- if self.transmission_mode == TransmissionMode.ACKNOWLEDGED:
- self._prepare_finished_ack_packet(finished_pdu.condition_code)
- self.states.step = TransactionStep.SENDING_ACK_OF_FINISHED
- else:
- self.states.step = TransactionStep.NOTICE_OF_COMPLETION
-
- def _notice_of_completion(self):
- if self.cfg.indication_cfg.transaction_finished_indication_required:
- assert self._params.transaction_id is not None
- # This happens for unacknowledged file copy operation with no closure.
- if self._params.finished_params is None:
- self._params.finished_params = FinishedParams(
- condition_code=ConditionCode.NO_ERROR,
- delivery_code=DeliveryCode.DATA_COMPLETE,
- file_status=FileStatus.FILE_STATUS_UNREPORTED,
- )
- indication_params = TransactionFinishedParams(
- transaction_id=self._params.transaction_id,
- finished_params=self._params.finished_params,
- )
- self.user.transaction_finished_indication(indication_params)
- # Transaction finished
- self.reset()
-
- def _fsm_advancement_after_packets_were_sent(self):
- """Advance the internal FSM after all packets to be sent were retrieved from the handler."""
- if len(self._pdus_to_be_sent) > 0:
- raise UnretrievedPdusToBeSent(
- f"{len(self._pdus_to_be_sent)} packets left to send"
- )
- if self.states.step == TransactionStep.SENDING_METADATA:
- self.states.step = TransactionStep.SENDING_FILE_DATA
- elif self.states.step == TransactionStep.RETRANSMITTING:
- assert self._params.ack_params.step_before_retransmission is not None
- self.states.step = self._params.ack_params.step_before_retransmission
- elif self.states.step == TransactionStep.SENDING_FILE_DATA:
- self._handle_file_data_sent()
- elif self.states.step == TransactionStep.SENDING_ACK_OF_FINISHED:
- self.states.step = TransactionStep.NOTICE_OF_COMPLETION
- elif self.states.step == TransactionStep.SENDING_EOF:
- self._handle_eof_sent()
-
- def _handle_eof_sent(self):
- if self.cfg.indication_cfg.eof_sent_indication_required:
- assert self._params.transaction_id is not None
- self.user.eof_sent_indication(self._params.transaction_id)
- if self.transmission_mode == TransmissionMode.UNACKNOWLEDGED:
- if self._params.closure_requested:
- assert self._params.remote_cfg is not None
- self._params.check_timer = (
- self.check_timer_provider.provide_check_timer(
- local_entity_id=self.cfg.local_entity_id,
- remote_entity_id=self._params.remote_cfg.entity_id,
- entity_type=EntityType.SENDING,
- )
- )
- self.states.step = TransactionStep.WAITING_FOR_FINISHED
- else:
- self.states.step = TransactionStep.NOTICE_OF_COMPLETION
- else:
- self._start_positive_ack_procedure()
-
- def _handle_file_data_sent(self):
- if self._params.fp.progress == self._params.fp.file_size:
- self._params.cond_code_eof = ConditionCode.NO_ERROR
- self.states.step = TransactionStep.SENDING_EOF
-
- def _prepare_finished_ack_packet(self, condition_code: ConditionCode):
- ack_pdu = AckPdu(
- self._params.pdu_conf,
- DirectiveType.FINISHED_PDU,
- condition_code,
- TransactionStatus.ACTIVE,
- )
- self._add_packet_to_be_sent(ack_pdu)
-
- def _start_positive_ack_procedure(self):
- assert self._params.remote_cfg is not None
- self.states.step = TransactionStep.WAITING_FOR_EOF_ACK
- self._params.positive_ack_params.ack_timer = Countdown.from_seconds(
- self._params.remote_cfg.positive_ack_timer_interval_seconds
- )
- self._params.positive_ack_params.ack_counter = 0
-
- def _setup_transmission_mode(self):
- assert self._put_req is not None
- assert self._params.remote_cfg is not None
- # Transmission mode settings in the put request override settings from the remote MIB
- trans_mode_to_set = self._put_req.trans_mode
- if trans_mode_to_set is None:
- trans_mode_to_set = self._params.remote_cfg.default_transmission_mode
- closure_req_to_set = self._put_req.closure_requested
- if closure_req_to_set is None:
- closure_req_to_set = self._params.remote_cfg.closure_requested
- # This also sets the field of the PDU configuration struct.
- self._params.transmission_mode = trans_mode_to_set
- self._params.closure_requested = closure_req_to_set
- self._crc_helper.checksum_type = self._params.remote_cfg.crc_type
-
- def _add_packet_to_be_sent(self, packet: GenericPduPacket):
- self._pdus_to_be_sent.append(PduHolder(packet))
- self.states._num_packets_ready += 1
-
- def _prepare_progressing_file_data_pdu(self) -> bool:
- """Prepare the next file data PDU, which also progresses the file copy operation.
-
- :return: True if a packet was prepared, False if PDU handling is done and the next steps
- in the Copy File procedure can be performed
- """
- # No need to send a file data PDU for an empty file
- if self._params.fp.metadata_only:
- return False
- if self._params.fp.progress == self._params.fp.file_size:
- return False
- if self._params.fp.file_size < self._params.fp.segment_len:
- read_len = self._params.fp.file_size
- else:
- if (
- self._params.fp.progress + self._params.fp.segment_len
- > self._params.fp.file_size
- ):
- read_len = self._params.fp.file_size - self._params.fp.progress
- else:
- read_len = self._params.fp.segment_len
- self._prepare_file_data_pdu(self._params.fp.progress, read_len)
- self._params.fp.progress += read_len
- return True
-
- def _prepare_file_data_pdu(self, offset: int, read_len: int):
- """Generic function to prepare a file data PDU. This function can also be used to
- re-transmit file data PDUs of segments which were already sent."""
- assert self._put_req is not None
- assert self._put_req.source_file is not None
- with open(self._put_req.source_file, "rb") as of:
- file_data = self.user.vfs.read_from_opened_file(of, offset, read_len)
- # TODO: Support for record continuation state not implemented yet. Segment metadata
- # flag is therefore always set to False. Segment metadata support also omitted
- # for now. Implementing those generically could be done in form of a callback,
- # e.g. abstractmethod of this handler as a first way, another one being
- # to expect the user to supply some helper class to split up a file
- fd_params = FileDataParams(
- file_data=file_data, offset=offset, segment_metadata=None
- )
- file_data_pdu = FileDataPdu(
- pdu_conf=self._params.pdu_conf, params=fd_params
- )
- self._add_packet_to_be_sent(file_data_pdu)
-
- def _prepare_eof_pdu(self, checksum: bytes):
- assert self._params.cond_code_eof is not None
- self._add_packet_to_be_sent(
- EofPdu(
- file_checksum=checksum,
- file_size=self._params.fp.progress,
- pdu_conf=self._params.pdu_conf,
- condition_code=self._params.cond_code_eof,
- )
- )
-
- def _get_next_transfer_seq_num(self):
- next_seq_num = self.seq_num_provider.get_and_increment()
- if self.seq_num_provider.max_bit_width not in [8, 16, 32]:
- raise ValueError(
- "Invalid bit width for sequence number provider, must be one of [8,"
- " 16, 32]"
- )
- self._params.pdu_conf.transaction_seq_num = ByteFieldGenerator.from_int(
- self.seq_num_provider.max_bit_width // 8, next_seq_num
- )
-
- def _declare_fault(self, cond: ConditionCode):
- fh = self.cfg.default_fault_handlers.get_fault_handler(cond)
- transaction_id = self._params.transaction_id
- progress = self._params.fp.progress
- assert transaction_id is not None
- if fh == FaultHandlerCode.NOTICE_OF_CANCELLATION:
- if not self._notice_of_cancellation(cond):
- return
- elif fh == FaultHandlerCode.NOTICE_OF_SUSPENSION:
- self._notice_of_suspension()
- elif fh == FaultHandlerCode.ABANDON_TRANSACTION:
- self._abandon_transaction()
- self.cfg.default_fault_handlers.report_fault(transaction_id, cond, progress)
-
- def _notice_of_cancellation(self, condition_code: ConditionCode) -> bool:
- """Returns whether the fault declaration handler can returns prematurely."""
- # CFDP standard 4.11.2.2.3: Any fault declared in the course of transferring
- # the EOF (cancel) PDU must result in abandonment of the transaction.
- if (
- self._params.cond_code_eof is not None
- and self._params.cond_code_eof != ConditionCode.NO_ERROR
- ):
- assert self._params.transaction_id is not None
- # We still call the abandonment callback to ensure the fault is logged.
- self.cfg.default_fault_handlers.abandoned_cb(
- self._params.transaction_id,
- self._params.cond_code_eof,
- self._params.fp.progress,
- )
- self._abandon_transaction()
- return False
- self._params.cond_code_eof = condition_code
- # As specified in 4.11.2.2, prepare an EOF PDU to be sent to the remote entity. Supply
- # the checksum for the file copy progress sent so far.
- self._prepare_eof_pdu(self._checksum_calculation(self._params.fp.progress))
- self.states.step = TransactionStep.SENDING_EOF
- return True
-
- def _notice_of_suspension(self):
- # TODO: Implement
- pass
-
- def _abandon_transaction(self):
- # I guess an abandoned transaction just stops whatever it is doing.. The implementation
- # for this is quite easy.
- self.reset()
-
- def _checksum_calculation(self, size_to_calculate: int) -> bytes:
- if self._params.fp.file_size == 0:
- # Empty file, use null checksum
- crc = NULL_CHECKSUM_U32
- else:
- assert self._put_req is not None
- assert self._put_req.source_file is not None
- crc = self._crc_helper.calc_for_file(
- file_path=self._put_req.source_file,
- file_sz=size_to_calculate,
- segment_len=self._params.fp.segment_len,
- )
- return crc
-
- @deprecated(
- version="6.0.0rc1",
- reason="use insert_packet instead",
- )
- def pass_packet(self, packet: AbstractFileDirectiveBase):
- self.insert_packet(packet)
diff --git a/tmtccmd/cfdp/mib.py b/tmtccmd/cfdp/mib.py
deleted file mode 100644
index 373a3a12..00000000
--- a/tmtccmd/cfdp/mib.py
+++ /dev/null
@@ -1,285 +0,0 @@
-import abc
-import enum
-from abc import ABC
-from dataclasses import dataclass
-from typing import Optional, Dict, Sequence
-
-from spacepackets.cfdp.defs import (
- FaultHandlerCode,
- ChecksumType,
- TransmissionMode,
- CFDP_VERSION_2,
- ConditionCode,
- TransactionId,
-)
-from spacepackets.util import UnsignedByteField
-from tmtccmd.util.countdown import Countdown
-
-
-class DefaultFaultHandlerBase(ABC):
- """This base class provides a way to implement the fault handling procedures as specified
- in chapter 4.8 of the CFDP standard.
-
- It is passed into the CFDP handlers as part of the local entity configuration and provides
- a way to specify custom user error handlers.
-
- It does so by mapping each applicable CFDP :py:class:`ConditionCode` to a fault handler which
- is denoted by the four :py:class:`spacepackets.cfdp.defs.FaultHandlerCode` s. This code is used
- to dispatch to a user-provided callback function:
-
- 1. ``IGNORE_ERROR`` -> :py:meth:`ignore_cb`
- 2. ``NOTICE_OF_CANCELLATION`` -> :py:meth:`notice_of_cancellation_cb`
- 3. ``NOTICE_OF_SUSPENSION`` -> :py:meth:`notice_of_suspension_cb`
- 4. ``ABANDON_TRANSACTION`` -> :py:meth:`abandon_transaction_cb`
-
- For each error reported by :py:meth:`report_fault`, the appropriate fault handler callback
- will be called. The user provides the callbacks by providing a custom class which implements
- this base class and all abstract fault handler callbacks. This allows logging of the errors
- as specified in chapter 4.8.3.
-
- Some note on the provided default settings:
-
- - Checksum failures will be ignored by default. This is because for unacknowledged transfers,
- cancelling the transfer immediately would interfere with the check limit mechanism specified
- in chapter 4.6.3.3.
- - Unsupported checksum types will also be ignored by default. Even if the checksum type is
- not supported the file transfer might still have worked properly.
-
- """
-
- def __init__(self):
- # The initial default handle will be to cancel the transaction
- self._handler_dict: Dict[ConditionCode, FaultHandlerCode] = {
- ConditionCode.CANCEL_REQUEST_RECEIVED: FaultHandlerCode.NOTICE_OF_CANCELLATION,
- ConditionCode.POSITIVE_ACK_LIMIT_REACHED: FaultHandlerCode.NOTICE_OF_CANCELLATION,
- ConditionCode.KEEP_ALIVE_LIMIT_REACHED: FaultHandlerCode.NOTICE_OF_CANCELLATION,
- ConditionCode.INVALID_TRANSMISSION_MODE: FaultHandlerCode.NOTICE_OF_CANCELLATION,
- ConditionCode.FILE_CHECKSUM_FAILURE: FaultHandlerCode.IGNORE_ERROR,
- ConditionCode.FILE_SIZE_ERROR: FaultHandlerCode.NOTICE_OF_CANCELLATION,
- ConditionCode.FILESTORE_REJECTION: FaultHandlerCode.NOTICE_OF_CANCELLATION,
- ConditionCode.NAK_LIMIT_REACHED: FaultHandlerCode.NOTICE_OF_CANCELLATION,
- ConditionCode.INACTIVITY_DETECTED: FaultHandlerCode.NOTICE_OF_CANCELLATION,
- ConditionCode.CHECK_LIMIT_REACHED: FaultHandlerCode.NOTICE_OF_CANCELLATION,
- ConditionCode.UNSUPPORTED_CHECKSUM_TYPE: FaultHandlerCode.IGNORE_ERROR,
- }
-
- def get_fault_handler(self, condition: ConditionCode) -> Optional[FaultHandlerCode]:
- return self._handler_dict.get(condition)
-
- def set_handler(self, condition: ConditionCode, handler: FaultHandlerCode):
- """
- Raises
- -------
-
- ValueError
- Invalid condition code which is not applicable for fault handling procedures.
- """
- if condition not in self._handler_dict:
- raise ValueError(
- f"condition code {condition!r} not applicable for fault handling procedures"
- )
- self._handler_dict.update({condition: handler})
-
- def report_fault(
- self, transaction_id: TransactionId, condition: ConditionCode, progress: int
- ):
- """
- Raises
- -------
-
- ValueError
- Invalid condition code which is not applicable for fault handling procedures.
- """
- if condition not in self._handler_dict:
- raise ValueError(
- f"condition code {condition!r} not applicable for fault handling procedures"
- )
- fh_code = self._handler_dict.get(condition)
- if fh_code == FaultHandlerCode.NOTICE_OF_CANCELLATION:
- self.notice_of_cancellation_cb(transaction_id, condition, progress)
- elif fh_code == FaultHandlerCode.NOTICE_OF_SUSPENSION:
- self.notice_of_suspension_cb(transaction_id, condition, progress)
- elif fh_code == FaultHandlerCode.IGNORE_ERROR:
- self.ignore_cb(transaction_id, condition, progress)
- elif fh_code == FaultHandlerCode.ABANDON_TRANSACTION:
- self.abandoned_cb(transaction_id, condition, progress)
-
- @abc.abstractmethod
- def notice_of_suspension_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- pass
-
- @abc.abstractmethod
- def notice_of_cancellation_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- pass
-
- @abc.abstractmethod
- def abandoned_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- pass
-
- @abc.abstractmethod
- def ignore_cb(
- self, transaction_id: TransactionId, cond: ConditionCode, progress: int
- ):
- pass
-
-
-class EntityType(enum.IntEnum):
- SENDING = 0
- RECEIVING = 1
-
-
-class CheckTimerProvider(ABC):
- @abc.abstractmethod
- def provide_check_timer(
- self,
- local_entity_id: UnsignedByteField,
- remote_entity_id: UnsignedByteField,
- entity_type: EntityType,
- ) -> Countdown:
- pass
-
-
-@dataclass
-class IndicationCfg:
- eof_sent_indication_required: bool = True
- eof_recv_indication_required: bool = True
- file_segment_recvd_indication_required: bool = True
- transaction_finished_indication_required: bool = True
- suspended_indication_required: bool = True
- resumed_indication_required: bool = True
-
-
-@dataclass
-class LocalEntityCfg:
- """This models the remote entity configuration information as specified in chapter 8.2
- of the CFDP standard."""
-
- local_entity_id: UnsignedByteField
- indication_cfg: IndicationCfg
- default_fault_handlers: DefaultFaultHandlerBase
-
-
-@dataclass
-class RemoteEntityCfg:
- """This models the remote entity configuration information as specified in chapter 8.3
- of the CFDP standard.
-
- Some of the fields which were not considered necessary for the Python implementation
- were omitted. Some other fields which are not contained inside the standard but are considered
- necessary for the Python implementation are included.
-
- **Notes on Positive Acknowledgment Procedures**
-
- The ``positive_ack_timer_interval_seconds`` and ``positive_ack_timer_expiration_limit`` will
- be used for positive acknowledgement procedures as specified in CFDP chapter 4.7. The sending
- entity will start the timer for any PDUs where an acknowledgment is required (e.g. EOF PDU).
- Once the expected ACK response has not been received for that interval, as counter will be
- incremented and the timer will be reset. Once the counter exceeds the
- ``positive_ack_timer_expiration_limit``, a Positive ACK Limit Reached fault will be declared.
-
- **Notes on Deferred Lost Segment Procedures**
-
- This procedure will be active if an EOF (No Error) PDU is received in acknowledged mode. After
- issuing the NAK sequence which has the whole file scope, a timer will be started. The timer is
- reset when missing segments or missing metadata is received. The timer will be deactivated if
- all missing data is received. If the timer expires, a new NAK sequence will be issued and a
- counter will be incremented, which can lead to a NAK Limit Reached fault being declared.
-
- Parameters
- -----------
-
- entity_id
- The ID of the remote entity.
- max_packet_len
- This determines of all PDUs generated for that remote entity in addition to the
- `max_file_segment_len` attribute which also determines the size of file data PDUs.
- max_file_segment_len
- The maximum file segment length which determines the maximum size
- of file data PDUs in addition to the `max_packet_len` attribute. If this field is set
- to None, the maximum file segment length will be derived from the maximum packet length.
- If this has some value which is smaller than the segment value derived from
- `max_packet_len`, this value will be picked.
- closure_requested
- If the closure requested field is not supplied as part of the Put Request, it will be
- determined from this field in the remote configuration.
- crc_on_transmission
- If the CRC option is not supplied as part of the Put Request, it will be
- determined from this field in the remote configuration.
- default_transmission_mode
- If the transmission mode is not supplied as part of the Put Request, it will be
- determined from this field in the remote configuration.
- disposition_on_cancellation
- Determines whether an incomplete received file is discard on transaction cancellation.
- Defaults to False.
- crc_type
- Default checksum type used to calculate for all file transmissions to this remote entity.
- check_limit
- this timer determines the expiry period for incrementing a check counter after an EOF PDU
- is received for an incomplete file transfer. This allows out-of-order reception of file
- data PDUs and EOF PDUs. Also see 4.6.3.3 of the CFDP standard. Defaults to 2, so the
- check limit timer may expire twice.
- positive_ack_timer_interval_seconds
- See the notes on the Positive Acknowledgment Procedures inside the class documentation.
- Expected as floating point seconds. Defaults to 10 seconds.
- positive_ack_timer_expiration_limit
- See the notes on the Positive Acknowledgment Procedures inside the class documentation.
- Defaults to 2, so the timer may expire twice.
- immediate_nak_mode:
- Specifies whether a NAK sequence should be issued immediately when a file data gap or
- lost metadata is detected in the acknowledged mode. Defaults to True.
- nak_timer_interval_seconds:
- See the notes on the Deferred Lost Segment Procedure inside the class documentation.
- Expected as floating point seconds. Defaults to 10 seconds.
- nak_timer_expiration_limit:
- See the notes on the Deferred Lost Segment Procedure inside the class documentation.
- Defaults to 2, so the timer may expire two times.
-
- """
-
- entity_id: UnsignedByteField
- max_file_segment_len: Optional[int]
- max_packet_len: int
- closure_requested: bool
- crc_on_transmission: bool
- default_transmission_mode: TransmissionMode
- crc_type: ChecksumType
- positive_ack_timer_interval_seconds: float = 10.0
- positive_ack_timer_expiration_limit: int = 2
- check_limit: int = 2
- disposition_on_cancellation: bool = False
- immediate_nak_mode: bool = True
- nak_timer_interval_seconds: float = 10.0
- nak_timer_expiration_limit: int = 2
- # NOTE: Only this version is supported
- cfdp_version: int = CFDP_VERSION_2
-
-
-class RemoteEntityCfgTable:
- """Thin abstraction for a dictionary containing remote configurations with the remote entity ID
- being used as a key."""
-
- def __init__(self, init_cfgs: Optional[Sequence[RemoteEntityCfg]] = None):
- self._remote_entity_dict = dict()
- if init_cfgs is not None:
- self.add_configs(init_cfgs)
-
- def add_config(self, cfg: RemoteEntityCfg) -> bool:
- if cfg.entity_id in self._remote_entity_dict:
- return False
- self._remote_entity_dict.update({cfg.entity_id.value: cfg})
- return True
-
- def add_configs(self, cfgs: Sequence[RemoteEntityCfg]):
- for cfg in cfgs:
- if cfg.entity_id in self._remote_entity_dict:
- continue
- self._remote_entity_dict.update({cfg.entity_id.value: cfg})
-
- def get_cfg(self, remote_entity_id: UnsignedByteField) -> Optional[RemoteEntityCfg]:
- return self._remote_entity_dict.get(remote_entity_id.value)
diff --git a/tmtccmd/cfdp/request.py b/tmtccmd/cfdp/request.py
index e17a8e0a..d2940de0 100644
--- a/tmtccmd/cfdp/request.py
+++ b/tmtccmd/cfdp/request.py
@@ -1,19 +1,6 @@
-from pathlib import Path
-from typing import Optional, List, cast
-
-from spacepackets.cfdp import (
- SegmentationControl,
- TransmissionMode,
- FaultHandlerOverrideTlv,
- FlowLabelTlv,
- MessageToUserTlv,
- FileStoreRequestTlv,
-)
-from spacepackets.cfdp.tlv import ProxyMessageType, ReservedCfdpMessage
-from spacepackets.util import UnsignedByteField
-from tmtccmd.cfdp.defs import CfdpRequestType
-import dataclasses
+from typing import Optional, cast
+from cfdppy.defs import CfdpRequestType
from tmtccmd.config.defs import CfdpParams
@@ -22,123 +9,6 @@ def __init__(self, req_type: CfdpRequestType):
self.req_type = req_type
-@dataclasses.dataclass
-class PutRequest:
- """This is the base class modelling put request. You can create this class from the simplified
- :py:class:`tmtccmd.config.defs.CfdpParams` class with the generic
- :py:func:`tmtccmd.config.cfdp.generic_cfdp_params_to_put_request` API and/or all related specific
- APIs."""
-
- destination_id: UnsignedByteField
- # All the following fields are optional because a put request can also be a metadata-only
- # request
- source_file: Optional[Path]
- dest_file: Optional[Path]
- trans_mode: Optional[TransmissionMode]
- closure_requested: Optional[bool]
- seg_ctrl: Optional[
- SegmentationControl
- ] = SegmentationControl.NO_RECORD_BOUNDARIES_PRESERVATION
- fault_handler_overrides: Optional[List[FaultHandlerOverrideTlv]] = None
- flow_label_tlv: Optional[FlowLabelTlv] = None
- msgs_to_user: Optional[List[MessageToUserTlv]] = None
- fs_requests: Optional[List[FileStoreRequestTlv]] = None
-
- @property
- def metadata_only(self):
- if self.source_file is None and self.dest_file is None:
- return True
- return False
-
- def __str__(self):
- src_file_str = "Unknown source file"
- dest_file_str = "Unknown destination file"
- if not self.metadata_only:
- src_file_str = f"Source File: {self.source_file}"
- dest_file_str = f"Destination File: {self.dest_file}"
- if self.trans_mode is not None:
- if self.trans_mode == TransmissionMode.ACKNOWLEDGED:
- trans_mode_str = "Transmission Mode: Class 2 Acknowledged"
- else:
- trans_mode_str = "Transmission Mode: Class 1 Unacknowledged"
- else:
- trans_mode_str = "Transmission Mode from MIB"
- if self.closure_requested is not None:
- if self.closure_requested:
- closure_str = "Closure requested"
- else:
- closure_str = "No closure requested"
- else:
- closure_str = "Closure Requested from MIB"
- if not self.metadata_only:
- print_str = (
- f"Destination ID {self.destination_id.value}\n\t"
- f"{src_file_str}\n\t{dest_file_str}\n\t{trans_mode_str}\n\t{closure_str}"
- )
- else:
- print_str = self.__str_for_metadata_only()
- return print_str
-
- def __str_for_metadata_only(self) -> str:
- print_str = (
- f"Metadata Only Put Request with Destination ID {self.destination_id.value}"
- )
- if self.msgs_to_user is not None:
- for idx, msg_to_user in enumerate(self.msgs_to_user):
- msg_to_user = cast(MessageToUserTlv, msg_to_user)
- if msg_to_user.is_reserved_cfdp_message():
- reserved_msg = msg_to_user.to_reserved_msg_tlv()
- assert reserved_msg is not None
- print_str = PutRequest.__str_for_reserved_cfdp_msg(
- idx, reserved_msg, print_str
- )
- return print_str
-
- @staticmethod
- def __str_for_reserved_cfdp_msg(
- idx: int, reserved_msg: ReservedCfdpMessage, print_str: str
- ) -> str:
- if reserved_msg.is_cfdp_proxy_operation():
- proxy_msg_type = reserved_msg.get_cfdp_proxy_message_type()
- print_str += f"\nMessage to User {idx}: Proxy operation {proxy_msg_type!r}"
- if proxy_msg_type == ProxyMessageType.PUT_REQUEST:
- print_str = PutRequest.__str_for_put_req(reserved_msg, print_str)
- elif proxy_msg_type == ProxyMessageType.PUT_RESPONSE:
- print_str = PutRequest.__str_for_put_response(reserved_msg, print_str)
- elif reserved_msg.is_originating_transaction_id():
- print_str += (
- f"\nMessage to User {idx}: Originating Transaction ID "
- f"{reserved_msg.get_originating_transaction_id()}"
- )
- return print_str
-
- @staticmethod
- def __str_for_put_req(reserved_msg: ReservedCfdpMessage, print_str: str) -> str:
- put_request_params = reserved_msg.get_proxy_put_request_params()
- assert put_request_params is not None
- print_str += (
- f"\n\tProxy Put Dest Entity ID: {put_request_params.dest_entity_id.value}"
- )
- print_str += (
- f"\n\tSource file: {put_request_params.source_file_name.value.decode()}"
- )
- print_str += (
- f"\n\tDest file: {put_request_params.dest_file_name.value.decode()}"
- )
- return print_str
-
- @staticmethod
- def __str_for_put_response(
- reserved_msg: ReservedCfdpMessage, print_str: str
- ) -> str:
- put_response_params = reserved_msg.get_proxy_put_response_params()
- assert put_response_params is not None
- print_str += f"\n\tCondition Code: {put_response_params.condition_code!r}"
- print_str += f"\n\tDelivery Code: {put_response_params.delivery_code!r}"
- print_str += f"\n\tFile Status: {put_response_params.file_status!r}"
- return print_str
-
-
class PutRequestCfgWrapper(CfdpRequestBase):
def __init__(self, cfg: CfdpParams):
super().__init__(CfdpRequestType.PUT)
diff --git a/tmtccmd/cfdp/user.py b/tmtccmd/cfdp/user.py
deleted file mode 100644
index 8802d04a..00000000
--- a/tmtccmd/cfdp/user.py
+++ /dev/null
@@ -1,166 +0,0 @@
-import logging
-from abc import abstractmethod, ABC
-from dataclasses import dataclass
-from typing import List, Optional, Any
-
-from spacepackets.cfdp.defs import ConditionCode, TransactionId
-from spacepackets.cfdp.tlv import MessageToUserTlv
-from spacepackets.cfdp.pdu.file_data import SegmentMetadata
-from spacepackets.cfdp.pdu.finished import FinishedParams
-from spacepackets.util import UnsignedByteField
-from tmtccmd.cfdp.filestore import VirtualFilestore, HostFilestore
-
-_LOGGER = logging.getLogger(__name__)
-
-
-@dataclass
-class TransactionParams:
- """Not wholly standard conformant here, but supplying the originating transaction ID
- makes the implementation of handling with proxy put requests easier."""
-
- transaction_id: TransactionId
- originating_transaction_id: Optional[TransactionId] = None
-
-
-@dataclass
-class MetadataRecvParams:
- transaction_id: TransactionId
- source_id: UnsignedByteField
- file_size: Optional[int]
- source_file_name: Optional[str]
- dest_file_name: Optional[str]
- msgs_to_user: Optional[List[MessageToUserTlv]] = None
-
-
-@dataclass
-class TransactionFinishedParams:
- transaction_id: TransactionId
- finished_params: FinishedParams
- status_report: Optional[Any] = None
-
-
-@dataclass
-class FileSegmentRecvdParams:
- """The length of the segment metadata is not supplied as an extra parameter as it can be
- simply queried with len(segment_metadata)
- """
-
- transaction_id: TransactionId
- offset: int
- length: int
- segment_metadata: Optional[SegmentMetadata]
-
-
-class CfdpUserBase(ABC):
- """This user base class provides the primary user interface to interact with CFDP handlers.
- It is also used to pass the Virtual Filestore (VFS) implementation to the CFDP handlers
- so the filestore operations can be mapped to the underlying filestore.
-
- This class is used by implementing it in a child class and then passing it to the CFDP
- handler objects. The base class provides default implementation for the user indication
- primitives specified in the CFDP standard. The user can override these implementations
- to provide custom indication handlers.
- """
-
- def __init__(self, vfs: Optional[VirtualFilestore] = None):
- if vfs is None:
- vfs = HostFilestore()
- self.vfs = vfs
-
- @abstractmethod
- def transaction_indication(
- self,
- transaction_indication_params: TransactionParams,
- ):
- """This indication is used to report the transaction ID to the CFDP user"""
- _LOGGER.info(
- f"Transaction.indication for {transaction_indication_params.transaction_id}"
- )
-
- @abstractmethod
- def eof_sent_indication(self, transaction_id: TransactionId):
- _LOGGER.info(f"EOF-Sent.indication for {transaction_id}")
-
- @abstractmethod
- def transaction_finished_indication(self, params: TransactionFinishedParams):
- """This is the ``Transaction-Finished.Indication`` as specified in chapter 3.4.8 of the
- standard.
-
- The user implementation of this function could be used to keep a (failed) transaction
- history, which might be useful for the positive ACK procedures expected from a receiving
- CFDP entity."""
- _LOGGER.info(
- f"Transaction-Finished.indication for {params.transaction_id}. Parameters:"
- )
- print(params)
-
- @abstractmethod
- def metadata_recv_indication(self, params: MetadataRecvParams):
- _LOGGER.info(
- f"Metadata-Recv.indication for {params.transaction_id}. Parameters:"
- )
- print(params)
-
- @abstractmethod
- def file_segment_recv_indication(self, params: FileSegmentRecvdParams):
- _LOGGER.info(
- f"File-Segment-Recv.indication for {params.transaction_id}. Parameters:"
- )
- print(params)
-
- @abstractmethod
- def report_indication(self, transaction_id: TransactionId, status_report: Any):
- # TODO: p.28 of the CFDP standard specifies what information the status report parameter
- # could contain. I think it would be better to not hardcode the type of the status
- # report here, but something like Union[any, CfdpStatusReport] with CfdpStatusReport
- # being an implementation which supports all three information suggestions would be
- # nice
- pass
-
- @abstractmethod
- def suspended_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode
- ):
- _LOGGER.info(
- f"Suspended.indication for {transaction_id} | Condition Code: {cond_code}"
- )
-
- @abstractmethod
- def resumed_indication(self, transaction_id: TransactionId, progress: int):
- _LOGGER.info(
- f"Resumed.indication for {transaction_id} | Progress: {progress} bytes"
- )
-
- @abstractmethod
- def fault_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode, progress: int
- ):
- """This is the ``Fault.Indication`` as specified in chapter 3.4.14 of the
- standard.
-
- The user implementation of this function could be used to keep a (failed) transaction
- history, which might be useful for the positive ACK procedures expected from a receiving
- CFDP entity."""
- _LOGGER.warning(
- f"Fault.indication for {transaction_id} | Condition Code: {cond_code} | "
- f"Progress: {progress} bytes"
- )
-
- @abstractmethod
- def abandoned_indication(
- self, transaction_id: TransactionId, cond_code: ConditionCode, progress: int
- ):
- """This is the ``Fault.Indication`` as specified in chapter 3.4.15 of the
- standard.
-
- The user implementation of this function could be used to keep a (failed) transaction
- history, which might be useful for the positive ACK procedures expected from a receiving
- CFDP entity."""
- _LOGGER.warning(
- f"Abandoned.indication for {transaction_id} | Condition Code: {cond_code} |"
- f" Progress: {progress} bytes"
- )
-
- @abstractmethod
- def eof_recv_indication(self, transaction_id: TransactionId):
- _LOGGER.info(f"EOF-Recv.indication for {transaction_id}")
diff --git a/tmtccmd/com/dummy.py b/tmtccmd/com/dummy.py
index c067b378..3c139b76 100644
--- a/tmtccmd/com/dummy.py
+++ b/tmtccmd/com/dummy.py
@@ -52,7 +52,7 @@ def generate_reply_package(self):
seq_count=self.current_ssc,
verif_params=VerificationParams(
req_id=RequestId(
- self.last_tc.packet_id, self.last_tc.packet_seq_ctrl
+ self.last_tc.packet_id, self.last_tc.packet_seq_control
)
),
time_provider=current_time_stamp,
@@ -67,7 +67,7 @@ def generate_reply_package(self):
seq_count=self.current_ssc,
verif_params=VerificationParams(
req_id=RequestId(
- self.last_tc.packet_id, self.last_tc.packet_seq_ctrl
+ self.last_tc.packet_id, self.last_tc.packet_seq_control
)
),
time_provider=current_time_stamp,
@@ -91,7 +91,7 @@ def generate_reply_package(self):
seq_count=self.current_ssc,
verif_params=VerificationParams(
req_id=RequestId(
- self.last_tc.packet_id, self.last_tc.packet_seq_ctrl
+ self.last_tc.packet_id, self.last_tc.packet_seq_control
)
),
time_provider=current_time_stamp,
diff --git a/tmtccmd/config/__init__.py b/tmtccmd/config/__init__.py
index 50bd9506..9bebab2e 100644
--- a/tmtccmd/config/__init__.py
+++ b/tmtccmd/config/__init__.py
@@ -11,42 +11,43 @@
from pathlib import Path
from typing import Optional
+from cfdppy.request import PutRequest
from spacepackets.cfdp import CfdpLv
-from spacepackets.util import UnsignedByteField
from spacepackets.cfdp.tlv import ProxyPutRequest, ProxyPutRequestParams
-from tmtccmd.core import TmMode, TcMode
+from spacepackets.util import UnsignedByteField
+
+from tmtccmd.cfdp.request import PutRequestCfgWrapper
+from tmtccmd.core import TcMode, TmMode
+from tmtccmd.core.base import ModeWrapper
+from tmtccmd.tmtc.procedure import (
+ CfdpProcedureInfo,
+ DefaultProcedureInfo,
+ ProcedureWrapper,
+ TcProcedureType,
+)
from .args import (
- SetupParams,
- create_default_args_parser,
- add_default_tmtccmd_args,
- parse_default_tmtccmd_input_arguments,
DefaultProcedureParams,
PreArgsParsingWrapper,
ProcedureParamsWrapper,
+ SetupParams,
+ add_default_tmtccmd_args,
+ create_default_args_parser,
+ parse_default_tmtccmd_input_arguments,
)
from .defs import (
- CoreModeList,
- CoreModeConverter,
- CoreComInterfaces,
CORE_COM_IF_DICT,
- default_json_path,
- CoreServiceList,
- ComIfDictT,
CfdpParams,
+ ComIfDictT,
+ CoreComInterfaces,
+ CoreModeConverter,
+ CoreModeList,
+ CoreServiceList,
+ default_json_path,
)
-from .prompt import prompt_op_code, prompt_service
-from .tmtc import TmtcDefinitionWrapper, OpCodeEntry, OpCodeOptionBase, CmdTreeNode
from .hook import HookBase
-from tmtccmd.tmtc.procedure import (
- DefaultProcedureInfo,
- CfdpProcedureInfo,
- TcProcedureType,
- ProcedureWrapper,
-)
-from tmtccmd.cfdp.request import PutRequest, PutRequestCfgWrapper
-from tmtccmd.core.base import ModeWrapper
-
+from .prompt import prompt_op_code, prompt_service
+from .tmtc import CmdTreeNode, OpCodeEntry, OpCodeOptionBase, TmtcDefinitionWrapper
_LOGGER = logging.getLogger(__name__)
@@ -69,11 +70,11 @@ def get_global_hook_obj() -> Optional[HookBase]:
"""
try:
- from tmtccmd.core.globals_manager import get_global
- from tmtccmd.config.definitions import CoreGlobalIds
-
from typing import cast
+ from tmtccmd.config.definitions import CoreGlobalIds
+ from tmtccmd.core.globals_manager import get_global
+
hook_obj_raw = get_global(CoreGlobalIds.TMTC_HOOK)
if hook_obj_raw is None:
_LOGGER.error("Hook object is invalid!")
diff --git a/tmtccmd/config/cfdp.py b/tmtccmd/config/cfdp.py
index d1b13bf8..8129fbed 100644
--- a/tmtccmd/config/cfdp.py
+++ b/tmtccmd/config/cfdp.py
@@ -1,10 +1,11 @@
from pathlib import Path
from typing import Optional
+from cfdppy.request import PutRequest
from spacepackets.cfdp import CfdpLv
-from spacepackets.cfdp.tlv import ProxyPutRequestParams, ProxyPutRequest
+from spacepackets.cfdp.tlv import ProxyPutRequest, ProxyPutRequestParams
from spacepackets.util import UnsignedByteField
-from tmtccmd.cfdp.request import PutRequest
+
from tmtccmd.config.defs import CfdpParams
diff --git a/tmtccmd/fsfw/tmtc_printer.py b/tmtccmd/fsfw/tmtc_printer.py
index 9ad1adde..53ee078a 100644
--- a/tmtccmd/fsfw/tmtc_printer.py
+++ b/tmtccmd/fsfw/tmtc_printer.py
@@ -7,7 +7,7 @@
from spacepackets.util import get_printable_data_string, PrintFormats
from tmtccmd.pus.s8_fsfw_action import Service8FsfwTm
-from tmtccmd.tmtc.base import PusTmInfoInterface, PusTmInterface
+from tmtccmd.tmtc.tm_base import PusTmInfoInterface, PusTmInterface
from tmtccmd.util.obj_id import ObjectIdU32, ObjectIdBase
from tmtccmd.pus.tm.s3_hk_base import HkContentType
from tmtccmd.logging import get_current_time_string
diff --git a/tmtccmd/pus/tm/s20_fsfw_param.py b/tmtccmd/pus/tm/s20_fsfw_param.py
index 688c5c7c..97c36ca7 100644
--- a/tmtccmd/pus/tm/s20_fsfw_param.py
+++ b/tmtccmd/pus/tm/s20_fsfw_param.py
@@ -3,6 +3,7 @@
from typing import Optional
from spacepackets import SpacePacketHeader
+from spacepackets.ccsds.spacepacket import PacketId, PacketSeqCtrl
from spacepackets.ccsds.time import CdsShortTimestamp, CcsdsTimeProvider
from spacepackets.ecss import (
Ptc,
@@ -99,12 +100,9 @@ def from_tm(cls, tm: PusTelemetry):
Service20FsfwTm.__common_checks(instance.pus_tm)
return instance
- def pack(self) -> bytes:
+ def pack(self) -> bytearray:
return self.pus_tm.pack()
- def sp_header(self) -> SpacePacketHeader:
- return self.pus_tm.space_packet_header
-
@property
def time_provider(self) -> Optional[CcsdsTimeProvider]:
return self.pus_tm.time_provider
@@ -134,5 +132,23 @@ def empty(cls) -> Service20FsfwTm:
source_data=bytes([0, 0, 0, 0]),
)
- def __eq__(self, other: Service20FsfwTm):
+ @property
+ def sp_header(self) -> SpacePacketHeader:
+ return self.pus_tm.space_packet_header
+
+ @property
+ def ccsds_version(self) -> int:
+ return self.pus_tm.ccsds_version
+
+ @property
+ def packet_id(self) -> PacketId:
+ return self.pus_tm.packet_id
+
+ @property
+ def packet_seq_control(self) -> PacketSeqCtrl:
+ return self.pus_tm.packet_seq_control
+
+ def __eq__(self, other: object):
+ if not isinstance(other, Service20FsfwTm):
+ return False
return self.pus_tm == other.pus_tm
diff --git a/tmtccmd/pus/tm/s5_fsfw_event.py b/tmtccmd/pus/tm/s5_fsfw_event.py
index 4f2c03e5..cc8012cf 100644
--- a/tmtccmd/pus/tm/s5_fsfw_event.py
+++ b/tmtccmd/pus/tm/s5_fsfw_event.py
@@ -8,6 +8,7 @@
from typing import Optional
from spacepackets import SpacePacketHeader
+from spacepackets.ccsds.spacepacket import PacketId, PacketSeqCtrl
from spacepackets.ccsds.time import CcsdsTimeProvider
from spacepackets.ecss.defs import PusService
from spacepackets.ecss.pus_5_event import Subservice
@@ -84,7 +85,7 @@ def sp_header(self) -> SpacePacketHeader:
def time_provider(self) -> Optional[CcsdsTimeProvider]:
return self.pus_tm.time_provider
- def pack(self) -> bytes:
+ def pack(self) -> bytearray:
return self.pus_tm.pack()
@property
@@ -95,6 +96,18 @@ def service(self) -> int:
def subservice(self) -> int:
return self.pus_tm.subservice
+ @property
+ def ccsds_version(self) -> int:
+ return self.pus_tm.ccsds_version
+
+ @property
+ def packet_id(self) -> PacketId:
+ return self.pus_tm.packet_id
+
+ @property
+ def packet_seq_control(self) -> PacketSeqCtrl:
+ return self.pus_tm.packet_seq_control
+
@property
def source_data(self) -> bytes:
return self.pus_tm.source_data
@@ -131,10 +144,13 @@ def severity(self) -> Severity:
return Severity.MEDIUM
elif self.subservice == Subservice.TM_HIGH_SEVERITY_EVENT:
return Severity.HIGH
+ raise ValueError(f"invalid severity for subservice {self.subservice}")
@property
def event_definition(self) -> EventDefinition:
return EventDefinition.from_bytes(self.pus_tm.source_data)
- def __eq__(self, other: Service5Tm):
+ def __eq__(self, other: object):
+ if not isinstance(other, Service5Tm):
+ return False
return self.pus_tm == other.pus_tm
diff --git a/tmtccmd/tmtc/ccsds_seq_sender.py b/tmtccmd/tmtc/ccsds_seq_sender.py
index 91a91de0..5b3e289d 100644
--- a/tmtccmd/tmtc/ccsds_seq_sender.py
+++ b/tmtccmd/tmtc/ccsds_seq_sender.py
@@ -4,16 +4,17 @@
from datetime import timedelta
from typing import Optional
+from spacepackets.countdown import Countdown
+
+from tmtccmd.com import ComInterface
from tmtccmd.tmtc import (
+ ProcedureWrapper,
+ QueueEntryHelper,
TcQueueEntryBase,
TcQueueEntryType,
- QueueEntryHelper,
- ProcedureWrapper,
)
-from tmtccmd.tmtc.handler import TcHandlerBase, SendCbParams
+from tmtccmd.tmtc.handler import SendCbParams, TcHandlerBase
from tmtccmd.tmtc.queue import QueueWrapper
-from tmtccmd.com import ComInterface
-from tmtccmd.util.countdown import Countdown
class SenderMode(enum.IntEnum):
diff --git a/tmtccmd/tmtc/queue.py b/tmtccmd/tmtc/queue.py
index c6c7383d..4f36ec87 100644
--- a/tmtccmd/tmtc/queue.py
+++ b/tmtccmd/tmtc/queue.py
@@ -5,15 +5,15 @@
from collections import deque
from datetime import timedelta
from enum import Enum
-from typing import Optional, Deque, cast, Any, Type
-
+from typing import Any, Deque, Optional, Type, cast
from spacepackets.ccsds import SpacePacket
+from spacepackets.ecss import PusService, PusVerificator, check_pus_crc
from spacepackets.ecss.tc import PusTelecommand
-from spacepackets.ecss import PusVerificator, PusService, check_pus_crc
-from tmtccmd.tmtc.procedure import TcProcedureBase, DefaultProcedureInfo
-from tmtccmd.util import ProvidesSeqCount
+from spacepackets.seqcount import ProvidesSeqCount
+
from tmtccmd.pus.s11_tc_sched import Subservice as Pus11Subservice
+from tmtccmd.tmtc.procedure import DefaultProcedureInfo, TcProcedureBase
class TcQueueEntryType(Enum):
diff --git a/tmtccmd/util/__init__.py b/tmtccmd/util/__init__.py
index 28026ba0..5d696d93 100644
--- a/tmtccmd/util/__init__.py
+++ b/tmtccmd/util/__init__.py
@@ -1,8 +1,2 @@
from .obj_id import ObjectIdU32, ObjectIdU16, ObjectIdU8, ObjectIdBase, ObjectIdDictT
from .retval import RetvalDictT
-from .seqcnt import (
- FileSeqCountProvider,
- PusFileSeqCountProvider,
- ProvidesSeqCount,
- SeqCountProvider,
-)
diff --git a/tmtccmd/util/countdown.py b/tmtccmd/util/countdown.py
index 6a48df0f..c45937f2 100644
--- a/tmtccmd/util/countdown.py
+++ b/tmtccmd/util/countdown.py
@@ -1,87 +1,9 @@
-from __future__ import annotations
+from spacepackets.countdown import Countdown, time_ms # noqa: F401
-import time
-from typing import Optional
-from deprecated.sphinx import deprecated
-from datetime import timedelta
+import warnings
-
-def time_ms() -> int:
- return round(time.time() * 1000)
-
-
-class Countdown:
- def __init__(self, init_timeout: Optional[timedelta]):
- if init_timeout is not None:
- self._timeout_ms = int(init_timeout / timedelta(milliseconds=1))
- self._start_time_ms = time_ms()
- else:
- self._timeout_ms = 0
- self._start_time_ms = 0
-
- @classmethod
- def from_seconds(cls, timeout_seconds: float) -> Countdown:
- return cls(timedelta(seconds=timeout_seconds))
-
- @classmethod
- def from_millis(cls, timeout_ms: int) -> Countdown:
- return cls(timedelta(milliseconds=timeout_ms))
-
- @property
- def timeout_ms(self) -> int:
- """Returns timeout as integer milliseconds."""
- return self._timeout_ms
-
- @property
- def timeout(self) -> timedelta:
- return timedelta(milliseconds=self._timeout_ms)
-
- @timeout.setter
- def timeout(self, timeout: timedelta):
- """Set a new timeout for the countdown instance."""
- 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
-
- @deprecated(
- version="7.0.0",
- reason="use remaining_time method instead",
- )
- def rem_time(self):
- return self.remaining_time()
-
- def remaining_time(self) -> timedelta:
- """Remaining time left."""
- end_time = self._start_time_ms + self._timeout_ms
- current = time_ms()
- if end_time < current:
- return timedelta()
- return timedelta(milliseconds=end_time - current)
-
- def __repr__(self):
- return f"{self.__class__.__name__}(init_timeout={timedelta(milliseconds=self._timeout_ms)})"
-
- def __str__(self):
- return (
- f"{self.__class__.__class__} with"
- f" {timedelta(milliseconds=self._timeout_ms)} ms timeout,"
- f" {self.remaining_time()} time remaining"
- )
+warnings.warn(
+ "the countdown module is deprecated and was moved to spacepackets.countdown",
+ DeprecationWarning,
+ stacklevel=2,
+)
diff --git a/tmtccmd/util/seqcnt.py b/tmtccmd/util/seqcnt.py
index 88e439f4..47427e0c 100644
--- a/tmtccmd/util/seqcnt.py
+++ b/tmtccmd/util/seqcnt.py
@@ -1,105 +1,14 @@
-from abc import abstractmethod, ABC
-from pathlib import Path
-
-
-class ProvidesSeqCount(ABC):
- @property
- @abstractmethod
- def max_bit_width(self) -> int:
- pass
-
- @max_bit_width.setter
- @abstractmethod
- def max_bit_width(self, width: int):
- pass
-
- @abstractmethod
- def get_and_increment(self) -> int:
- """Contract: Retrieve the current sequence count and then increment it. The first call
- should yield 0"""
- raise NotImplementedError(
- "Please use a concrete class implementing this method"
- )
-
- def __next__(self):
- return self.get_and_increment()
-
-
-class FileSeqCountProvider(ProvidesSeqCount):
- """Sequence count provider which uses a disk file to store the current sequence count
- in a non-volatile way. The first call with the next built-in or using the base
- class :py:meth:`current` call will yield a 0
- """
-
- def __init__(self, max_bit_width: int, file_name: Path = Path("seqcnt.txt")):
- self.file_name = file_name
- self._max_bit_width = max_bit_width
- if not self.file_name.exists():
- self.create_new()
-
- @property
- def max_bit_width(self) -> int:
- return self._max_bit_width
-
- @max_bit_width.setter
- def max_bit_width(self, width: int):
- self._max_bit_width = width
-
- 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 get_and_increment(self) -> int:
- 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.check_count(file.readline())
- file.seek(0)
- file.write(f"{self._increment_with_rollover(curr_seq_cnt)}\n")
- return curr_seq_cnt
-
- def check_count(self, 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, self.max_bit_width) - 1:
- raise ValueError("Sequence count in file has invalid value")
- return curr_seq_cnt
-
- def _increment_with_rollover(self, seq_cnt: int) -> int:
- """CCSDS Sequence count has maximum size of 14 bit. Rollover after that size by default"""
- if seq_cnt >= pow(2, self.max_bit_width) - 1:
- return 0
- else:
- return seq_cnt + 1
-
-
-class PusFileSeqCountProvider(FileSeqCountProvider):
- def __init__(self, file_name: Path = Path("seqcnt.txt")):
- super().__init__(max_bit_width=14, file_name=file_name)
-
-
-class SeqCountProvider(ProvidesSeqCount):
- def __init__(self, bit_width: int):
- self.count = 0
- self._max_bit_width = bit_width
-
- @property
- def max_bit_width(self) -> int:
- return self._max_bit_width
-
- @max_bit_width.setter
- def max_bit_width(self, width: int):
- self._max_bit_width = width
-
- def get_and_increment(self) -> int:
- curr_count = self.count
- self.count += 1
- return curr_count
+from spacepackets.seqcount import ( # noqa: F401
+ ProvidesSeqCount,
+ FileSeqCountProvider,
+ PusFileSeqCountProvider,
+ SeqCountProvider,
+)
+
+import warnings
+
+warnings.warn(
+ "the countdown module is deprecated and was moved to spacepackets.countdown",
+ DeprecationWarning,
+ stacklevel=2,
+)