Repository: robamu-org/tmtccmd (language: python)

Issue:
Allow command definitions specification as (string) tree
The current approach of specifying command definitions is quite inflexible because it only allows one sublayer (op codes as the sublayer of the service).

A more flexible approach which would also be possible to implement in the GUI would be to allow specifying the command definitions in a tree format, where the user can build build a command definition tree, where a command path consists of  full path from root to a command leaf. For example, it could look like this

root
- core (n)
- - reboot (l)
- test (n)
- - ping (l)
- acs_subsystem
- tcs_subsystem
- - tcs_ctrl
- - - set_mode
- - poll_all_temps

The full path is passed to the user feed callback, for example as a slash or colon separated string, for example: `tcs_subsystem:tcs_ctrl:set_mode`. A tree like that would also be traversable in both CLI and the GUI.


Discussion / hints:

Fix patch:
diff --git a/docs/api.rst b/docs/api.rst
index 9bad825d..953b2c82 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -44,17 +44,6 @@ Module
 .. automodule:: tmtccmd
    :noindex:
 
-Configuration Package 
---------------------------
-
-.. automodule:: tmtccmd.config
-   :noindex:
-
-.. toctree::
-   :maxdepth: 3
-
-   api/config
-
 Core Package 
 -----------------
 
@@ -86,6 +75,17 @@ Telecommand Handling Package
 
    api/tc
 
+Application Configuration Package 
+===================================
+
+.. automodule:: tmtccmd.config
+   :noindex:
+
+.. toctree::
+   :maxdepth: 3
+
+   api/config
+
 Miscellaneous Modules 
 =========================
 
diff --git a/docs/api/config.rst b/docs/api/config.rst
index daa9cf82..5d0cc171 100644
--- a/docs/api/config.rst
+++ b/docs/api/config.rst
@@ -1,69 +1,76 @@
 Configuration Package
 ======================
 
-Module contents
----------------
+Module
+----------
 
 .. automodule:: tmtccmd.config
    :members:
    :undoc-members:
    :show-inheritance:
 
-Submodules
-----------
-
-tmtccmd.config.defs module
---------------------------
+Configuration Hook Submodule
+-----------------------------
 
-.. automodule:: tmtccmd.config.defs
+.. automodule:: tmtccmd.config.hook
    :members:
    :undoc-members:
    :show-inheritance:
 
-tmtccmd.config.args module
---------------------------
+Argument Parsing Submodule
+-----------------------------
 
 .. automodule:: tmtccmd.config.args
    :members:
    :undoc-members:
    :show-inheritance:
 
-tmtccmd.config.cfdp module
---------------------------
+TMTC Configuration Submodule
+-----------------------------
 
-.. automodule:: tmtccmd.config.cfdp
+.. automodule:: tmtccmd.config.tmtc
    :members:
    :undoc-members:
    :show-inheritance:
 
-tmtccmd.config.hook module
+CFDP Configuration Submodule
 -----------------------------
 
-.. automodule:: tmtccmd.config.hook
+.. automodule:: tmtccmd.config.cfdp
    :members:
    :undoc-members:
    :show-inheritance:
 
-tmtccmd.config.com module
------------------------------
+Communication Configuration Submodule
+---------------------------------------
 
 .. automodule:: tmtccmd.config.com
    :members:
    :undoc-members:
    :show-inheritance:
 
-tmtccmd.config.globals module
------------------------------
+Configuration Definitions Submodule
+------------------------------------
 
-.. automodule:: tmtccmd.config.globals
+.. automodule:: tmtccmd.config.defs
    :members:
    :undoc-members:
    :show-inheritance:
 
-tmtccmd.config.objects module
+Objects Submodule
 -----------------------------
 
 .. automodule:: tmtccmd.config.objects
    :members:
    :undoc-members:
    :show-inheritance:
+
+Global Module [deprecated]
+-----------------------------
+
+This module is deprecated, and usage is discouraged.
+
+.. automodule:: tmtccmd.config.globals
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/gettingstarted.rst b/docs/gettingstarted.rst
index 014b4f2e..a233b3ba 100644
--- a/docs/gettingstarted.rst
+++ b/docs/gettingstarted.rst
@@ -7,24 +7,119 @@ Example Project
 
 The `example application <https://github.com/robamu-org/tmtccmd/tree/main/examples/app>`_ is the
 best way to learn how this framework works and to get started. It shows how to set up handler
-classes for TC and TM handling and then ties together all components.
+classes for TC and TM handling and then ties together all components. You can also run this
+application in GUI mode by passing the ``-g`` GUI flag to the example application.
 
 Some explanation of classes and modules inside the example are given here.
 
-The example hook class
-______________________
+The Configuration Hook Class
+==============================
 
 The class `ExampleHookClass` is the example configuration class implementing
 the :py:class:`tmtccmd.config.hook.HookBase`.
 
-1. The :py:meth:`tmtccmd.config.hook.HookBase.assign_communication_interface` method
+1. The :py:meth:`tmtccmd.config.hook.HookBase.get_communication_interface` method
    is used to return a communication interface given a string identifier. You can read more
    about the communication abstraction in the :ref:`com` chapter.
-2. The :py:meth:`tmtccmd.config.hook.HookBase.get_tmtc_definitions` returns a configuration
-   wrapper which determines the commands displayed when using the interactive CLI mode or the GUI.
+2. The :py:meth:`tmtccmd.config.hook.HookBase.get_command_definitions` returns a tree of
+   command definition which can be used by both users and developers to specify the available
+   commands and procedures.
+
+TC Command Definition Specification
+--------------------------------------
+
+The command tree mechanism provides a flexible mechanism to also model the command definitions
+for more complex systems. These systems are oftentimes structured into dedicated modules.
+For exampe, the command tree for a satellite system might look like this:
+
+
+.. image:: images/example_system.png
+    :align: center
+
+
+For a system like this, it makes a lot of sense to also model the command definitions similarly
+to the system. An example tree modelling the system shown in the example above would look like this
+when printing one built using the :py:class:`tmtccmd.config.tmtc.CmdTreeNode`:
+
+.. code-block:: console
+
+  /                                   [ Root Node ]
+  ├── ping                            [ Send PUS ping command ]
+  ├── test                            [ Test Node ]
+  │  └── event                        [ Send PUS event test command ]
+  ├── system                          [ System Commands ]
+  ├── acs                             [ ACS Subsystem ]
+  │  ├── acs_ctrl                     [ ACS Controller ]
+  │  ├── mgt                          [ Magnetorquer ]
+  │     └── set_dipoles               [ Set MGT Dipoles ]
+  │  ├── mgm0                         [ Magnetometer 0 ]
+  │     └── other cmds                [ Other MGM commands ]
+  │  └── mgm1                         [ Magnetometer 1 ]
+  │     └── other cmds                [ Other MGM commands ]
+  ├── tcs                             [ TCS Subsystem ]
+  │  ├── tcs_ctrl                     [ TCS Controller ]
+  │  ├── pt1000                       [ Temperature Sensor ]
+  │  └── heater                       [ Heater ]
+  ├── com                             [ COM Subsystem ]
+  │  └── uhf_transceiver              [ UHF Transceiver ]
+  └── eps                             [ EPS Subsystem ]
+     └── pcdu                         [ PCDU ]
+        ├── channel_0_on              [ Channel 0 on ]
+        ├── channel_0_off             [ Channel 0 off ]
+        ├── channel_1_on              [ Channel 1 on ]
+        └── channel_1_off             [ Channel 1 off ]
+
+and the code to create this tree would look like this:
+
+.. code-block:: python
+
+    root_node = CmdTreeNode.root_node()
+    root_node.add_child(CmdTreeNode("ping", "Send PUS ping command"))
+    root_node.add_child(CmdTreeNode("test", "Test Node"))
+    root_node.children["test"].add_child(
+        CmdTreeNode("event", "Send PUS event test command")
+    )
+    root_node.add_child(CmdTreeNode("system", "System Commands"))
+    root_node.add_child(CmdTreeNode("acs", "ACS Subsystem"))
+    root_node["acs"].add_child(CmdTreeNode("acs_ctrl", "ACS Controller"))
+    root_node["acs"].add_child(CmdTreeNode("mgt", "Magnetorquer"))
+    root_node["acs"]["mgt"].add_child(CmdTreeNode("set_dipoles", "Set MGT Dipoles"))
+    root_node["acs"].add_child(CmdTreeNode("mgm0", "Magnetometer 0"))
+    root_node["acs"].add_child(CmdTreeNode("mgm1", "Magnetometer 1"))
+    mgm_node = CmdTreeNode("other cmds", "Other MGM commands")
+    root_node["acs"]["mgm0"].add_child(mgm_node)
+    root_node["acs"]["mgm1"].add_child(mgm_node)
+    root_node.add_child(CmdTreeNode("tcs", "TCS Subsystem"))
+    root_node["tcs"].add_child(CmdTreeNode("tcs_ctrl", "TCS Controller"))
+    root_node["tcs"].add_child(CmdTreeNode("pt1000", "Temperature Sensor"))
+    root_node["tcs"].add_child(CmdTreeNode("heater", "Heater"))
+    root_node.add_child(CmdTreeNode("com", "COM Subsystem"))
+    root_node["com"].add_child(CmdTreeNode("uhf_transceiver", "UHF Transceiver"))
+    root_node.add_child(CmdTreeNode("eps", "EPS Subsystem"))
+    root_node["eps"].add_child(CmdTreeNode("pcdu", "PCDU"))
+    root_node["eps"]["pcdu"].add_child(CmdTreeNode("channel_0_on", "Channel 0 on"))
+    root_node["eps"]["pcdu"].add_child(
+        CmdTreeNode("channel_0_off", "Channel 0 off")
+    )
+    root_node["eps"]["pcdu"].add_child(CmdTreeNode("channel_1_on", "Channel 1 on"))
+    root_node["eps"]["pcdu"].add_child(
+        CmdTreeNode("channel_1_off", "Channel 1 off")
+    )
+
+You could return this root node in your
+:py:meth:`tmtccmd.config.hook.HookBase.get_command_definitions` implementation.
+You can now specify your commands as command paths, which will then serve as identifier for single
+command or command stacks and procedures. The command path will be passed on as the `cmd_path`
+parameter of the :py:class:`tmtccmd.tmtc.procedure.DefaultProcedureInfo` which is passed to
+the :py:class:`tmtccmd.tmtc.handler.TcHandlerBase` implementation of the user.
+
+It is also possible to pass the command path as a CLI argument. For example, you can use
+``./tmtcc.py -p /test`` to send a ping command with the example application. Passing the
+command tree definition to the framework also allows it to provide a GUI command path selector
+for the GUI mode.
 
 The TC handler
----------------
+==============================
 
 This object is responsible for the telecommand handling. Therefore this object implements
 the :py:class:`tmtccmd.tmtc.handler.TcHandlerBase`.
@@ -40,7 +135,7 @@ the queue can also contain something like log requests or delay requests, or eve
 custom requests. These requests can then be handled by the user.
 
 The PUS TM handler
---------------------
+==============================
 
 This object is responsible for the handling of PUS telemetry. In the example case, the
 handler object is responsible space packets with a certain application process identifier (APID).
diff --git a/docs/images/example_system.drawio b/docs/images/example_system.drawio
new file mode 100644
index 00000000..9b780700
--- /dev/null
+++ b/docs/images/example_system.drawio
@@ -0,0 +1,194 @@
+<mxfile host="app.diagrams.net" modified="2023-11-20T17:53:13.730Z" agent="Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0" etag="LDF7sfEql8aMhhxCZOFH" version="22.1.3" type="device">
+  <diagram name="Page-1" id="uy2O-nbgk7QkcQA3I9NO">
+    <mxGraphModel dx="574" dy="329" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
+      <root>
+        <mxCell id="0" />
+        <mxCell id="1" parent="0" />
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-2" target="Qg0VGUPUKr4Bbsr4yj7K-11">
+          <mxGeometry relative="1" as="geometry">
+            <Array as="points">
+              <mxPoint x="320" y="210" />
+              <mxPoint x="285" y="210" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-15" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-2" target="Qg0VGUPUKr4Bbsr4yj7K-8">
+          <mxGeometry relative="1" as="geometry">
+            <Array as="points">
+              <mxPoint x="320" y="210" />
+              <mxPoint x="348" y="210" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-2" value="TCS" style="whiteSpace=wrap;html=1;align=center;direction=west;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
+          <mxGeometry x="290" y="180" width="60" height="20" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-19" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-3" target="Qg0VGUPUKr4Bbsr4yj7K-18">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-3" value="&lt;div&gt;&lt;br&gt;&lt;/div&gt;&lt;div&gt;EPS&lt;/div&gt;&lt;div&gt;&lt;br&gt;&lt;/div&gt;" style="whiteSpace=wrap;html=1;align=center;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
+          <mxGeometry x="200" y="180" width="60" height="20" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-5" target="Qg0VGUPUKr4Bbsr4yj7K-16">
+          <mxGeometry relative="1" as="geometry">
+            <Array as="points">
+              <mxPoint x="410" y="230" />
+              <mxPoint x="410" y="230" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-5" value="&lt;div&gt;COM&lt;/div&gt;" style="whiteSpace=wrap;html=1;align=center;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
+          <mxGeometry x="380" y="180" width="60" height="20" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-23" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-6" target="Qg0VGUPUKr4Bbsr4yj7K-21">
+          <mxGeometry relative="1" as="geometry">
+            <Array as="points">
+              <mxPoint x="160" y="210" />
+              <mxPoint x="120" y="210" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-43" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-6" target="Qg0VGUPUKr4Bbsr4yj7K-42">
+          <mxGeometry relative="1" as="geometry">
+            <Array as="points">
+              <mxPoint x="160" y="210" />
+              <mxPoint x="70" y="210" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-6" value="&lt;div&gt;ACS&lt;/div&gt;" style="whiteSpace=wrap;html=1;align=center;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
+          <mxGeometry x="130" y="180" width="60" height="20" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-40" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-8" target="Qg0VGUPUKr4Bbsr4yj7K-37">
+          <mxGeometry relative="1" as="geometry">
+            <Array as="points">
+              <mxPoint x="360" y="280" />
+              <mxPoint x="370" y="280" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-8" value="&lt;div&gt;Heater&lt;/div&gt;" style="whiteSpace=wrap;html=1;align=center;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
+          <mxGeometry x="320" y="230" width="55" height="40" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-39" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.44;exitY=0.015;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-11" target="Qg0VGUPUKr4Bbsr4yj7K-36">
+          <mxGeometry relative="1" as="geometry">
+            <Array as="points">
+              <mxPoint x="288" y="280" />
+              <mxPoint x="310" y="280" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-11" value="&lt;div&gt;PT1000&lt;/div&gt;" style="whiteSpace=wrap;html=1;align=center;direction=west;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
+          <mxGeometry x="260" y="230" width="50" height="40" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-16" value="&lt;div&gt;UHF Transceiver&lt;/div&gt;" style="whiteSpace=wrap;html=1;align=center;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
+          <mxGeometry x="380" y="230" width="70" height="40" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-31" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-18" target="Qg0VGUPUKr4Bbsr4yj7K-30">
+          <mxGeometry relative="1" as="geometry">
+            <Array as="points">
+              <mxPoint x="240" y="290" />
+              <mxPoint x="240" y="290" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-18" value="&lt;div&gt;PCDU&lt;/div&gt;" style="whiteSpace=wrap;html=1;align=center;direction=west;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
+          <mxGeometry x="210" y="230" width="40" height="40" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-35" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-20" target="Qg0VGUPUKr4Bbsr4yj7K-34">
+          <mxGeometry relative="1" as="geometry">
+            <Array as="points">
+              <mxPoint x="180" y="300" />
+              <mxPoint x="180" y="300" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-20" value="&lt;div&gt;MGMs&lt;/div&gt;" style="whiteSpace=wrap;html=1;align=center;direction=west;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
+          <mxGeometry x="150" y="230" width="50" height="40" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-33" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-21" target="Qg0VGUPUKr4Bbsr4yj7K-32">
+          <mxGeometry relative="1" as="geometry">
+            <Array as="points">
+              <mxPoint x="120" y="310" />
+              <mxPoint x="120" y="310" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-21" value="&lt;div&gt;MGT&lt;/div&gt;" style="whiteSpace=wrap;html=1;align=center;direction=west;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
+          <mxGeometry x="100" y="230" width="40" height="40" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-6" target="Qg0VGUPUKr4Bbsr4yj7K-20">
+          <mxGeometry relative="1" as="geometry">
+            <mxPoint x="180" y="230" as="targetPoint" />
+            <Array as="points">
+              <mxPoint x="160" y="210" />
+              <mxPoint x="180" y="210" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-25" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-24" target="Qg0VGUPUKr4Bbsr4yj7K-3">
+          <mxGeometry relative="1" as="geometry">
+            <mxPoint x="230" y="140" as="sourcePoint" />
+            <mxPoint x="240" y="180" as="targetPoint" />
+            <Array as="points">
+              <mxPoint x="260" y="160" />
+              <mxPoint x="230" y="160" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-26" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-24" target="Qg0VGUPUKr4Bbsr4yj7K-2">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-27" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-24" target="Qg0VGUPUKr4Bbsr4yj7K-5">
+          <mxGeometry relative="1" as="geometry">
+            <Array as="points">
+              <mxPoint x="260" y="160" />
+              <mxPoint x="410" y="160" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-24" target="Qg0VGUPUKr4Bbsr4yj7K-6">
+          <mxGeometry relative="1" as="geometry">
+            <Array as="points">
+              <mxPoint x="260" y="160" />
+              <mxPoint x="160" y="160" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-24" value="&lt;div&gt;System&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#b1ddf0;strokeColor=#10739e;" vertex="1" parent="1">
+          <mxGeometry x="230" y="110" width="60" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-30" value="&lt;div&gt;Channel(s) ON/OFF&lt;/div&gt;" style="whiteSpace=wrap;html=1;align=center;direction=west;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
+          <mxGeometry x="220" y="300" width="70" height="35" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-32" value="&lt;div&gt;Set&lt;br&gt;&lt;/div&gt;&lt;div&gt;Dipoles&lt;/div&gt;" style="whiteSpace=wrap;html=1;align=center;direction=west;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
+          <mxGeometry x="80" y="300" width="70" height="35" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-34" value="Other&lt;br&gt;CMDs" style="rounded=0;whiteSpace=wrap;html=1;dashed=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
+          <mxGeometry x="160" y="300" width="50" height="35" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-36" value="Other&lt;br&gt;CMDs" style="rounded=0;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
+          <mxGeometry x="295" y="300" width="50" height="35" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-37" value="Other&lt;br&gt;CMDs" style="rounded=0;whiteSpace=wrap;html=1;dashed=1;fillColor=#ffe6cc;strokeColor=#d79b00;" vertex="1" parent="1">
+          <mxGeometry x="360" y="300" width="50" height="35" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-38" value="Other&lt;br&gt;CMDs" style="rounded=0;whiteSpace=wrap;html=1;dashed=1;fillColor=#1ba1e2;fontColor=#ffffff;strokeColor=#006EAF;" vertex="1" parent="1">
+          <mxGeometry x="425" y="300" width="50" height="35" as="geometry" />
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-41" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="Qg0VGUPUKr4Bbsr4yj7K-16" target="Qg0VGUPUKr4Bbsr4yj7K-38">
+          <mxGeometry relative="1" as="geometry">
+            <mxPoint x="440" y="290" as="targetPoint" />
+            <Array as="points">
+              <mxPoint x="415" y="280" />
+              <mxPoint x="445" y="280" />
+            </Array>
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="Qg0VGUPUKr4Bbsr4yj7K-42" value="ACS&lt;br&gt;CTRL" style="whiteSpace=wrap;html=1;align=center;direction=west;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
+          <mxGeometry x="50" y="230" width="40" height="40" as="geometry" />
+        </mxCell>
+      </root>
+    </mxGraphModel>
+  </diagram>
+</mxfile>
diff --git a/docs/images/example_system.png b/docs/images/example_system.png
new file mode 100644
index 00000000..24a34049
Binary files /dev/null and b/docs/images/example_system.png differ
diff --git a/examples/app/tmtcc.py b/examples/app/tmtcc.py
index db19aefe..7bebd395 100755
--- a/examples/app/tmtcc.py
+++ b/examples/app/tmtcc.py
@@ -17,11 +17,9 @@
 from tmtccmd.pus import VerificationWrapper
 from tmtccmd.config import (
     default_json_path,
+    CmdTreeNode,
     SetupParams,
     HookBase,
-    TmtcDefinitionWrapper,
-    CoreServiceList,
-    OpCodeEntry,
     params_to_procedure_conversion,
     PreArgsParsingWrapper,
     SetupWrapper,
@@ -29,8 +27,6 @@
 from tmtccmd.logging import add_colorlog_console_logger
 from tmtccmd.logging.pus import (
     RegularTmtcLogWrapper,
-    RawTmtcTimedLogWrapper,
-    TimedLogWhen,
 )
 from tmtccmd.tmtc import (
     CcsdsTmHandler,
@@ -61,7 +57,7 @@ class ExampleHookClass(HookBase):
     def __init__(self, json_cfg_path: str):
         super().__init__(json_cfg_path=json_cfg_path)
 
-    def assign_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
+    def get_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
         assert self.cfg_path is not None
         print("Communication interface assignment function was called")
         from tmtccmd.config.com import (
@@ -78,27 +74,45 @@ def assign_communication_interface(self, com_if_key: str) -> Optional[ComInterfa
         assert cfg is not None
         return create_com_interface_default(cfg)
 
-    def get_tmtc_definitions(self) -> TmtcDefinitionWrapper:
-        from tmtccmd.config.globals import get_default_tmtc_defs
+    def get_command_definitions(self) -> CmdTreeNode:
+        root_node = CmdTreeNode.root_node()
+        root_node.add_child(CmdTreeNode("ping", "Send PUS ping command"))
+        root_node.add_child(CmdTreeNode("test", "Test Node"))
+        root_node.children["test"].add_child(
+            CmdTreeNode("event", "Send PUS event test command")
+        )
+        return self.build_more_complex_tree(root_node)
 
-        defs = get_default_tmtc_defs()
-        srv_5 = OpCodeEntry()
-        srv_5.add("0", "Event Test")
-        defs.add_service(
-            name=CoreServiceList.SERVICE_5.value,
-            info="PUS Service 5 Event",
-            op_code_entry=srv_5,
+    def build_more_complex_tree(self, root_node: CmdTreeNode) -> CmdTreeNode:
+        root_node.add_child(CmdTreeNode("system", "System Commands"))
+        root_node.add_child(CmdTreeNode("acs", "ACS Subsystem"))
+        root_node["acs"].add_child(CmdTreeNode("acs_ctrl", "ACS Controller"))
+        root_node["acs"].add_child(CmdTreeNode("mgt", "Magnetorquer"))
+        root_node["acs"]["mgt"].add_child(CmdTreeNode("set_dipoles", "Set MGT Dipoles"))
+        root_node["acs"].add_child(CmdTreeNode("mgm0", "Magnetometer 0"))
+        root_node["acs"].add_child(CmdTreeNode("mgm1", "Magnetometer 1"))
+        mgm_node = CmdTreeNode("other cmds", "Other MGM commands")
+        root_node["acs"]["mgm0"].add_child(mgm_node)
+        root_node["acs"]["mgm1"].add_child(mgm_node)
+        root_node.add_child(CmdTreeNode("tcs", "TCS Subsystem"))
+        root_node["tcs"].add_child(CmdTreeNode("tcs_ctrl", "TCS Controller"))
+        root_node["tcs"].add_child(CmdTreeNode("pt1000", "Temperature Sensor"))
+        root_node["tcs"].add_child(CmdTreeNode("heater", "Heater"))
+        root_node.add_child(CmdTreeNode("com", "COM Subsystem"))
+        root_node["com"].add_child(CmdTreeNode("uhf_transceiver", "UHF Transceiver"))
+        root_node.add_child(CmdTreeNode("eps", "EPS Subsystem"))
+        root_node["eps"].add_child(CmdTreeNode("pcdu", "PCDU"))
+        root_node["eps"]["pcdu"].add_child(CmdTreeNode("channel_0_on", "Channel 0 on"))
+        root_node["eps"]["pcdu"].add_child(
+            CmdTreeNode("channel_0_off", "Channel 0 off")
         )
-        srv_17 = OpCodeEntry()
-        srv_17.add("0", "Ping Test")
-        defs.add_service(
-            name=CoreServiceList.SERVICE_17_ALT,
-            info="PUS Service 17 Test",
-            op_code_entry=srv_17,
+        root_node["eps"]["pcdu"].add_child(CmdTreeNode("channel_1_on", "Channel 1 on"))
+        root_node["eps"]["pcdu"].add_child(
+            CmdTreeNode("channel_1_off", "Channel 1 off")
         )
-        return defs
+        return root_node
 
-    def perform_mode_operation(self, tmtc_backend: CcsdsTmtcBackend, mode: int):
+    def perform_mode_operation(self, _tmtc_backend: CcsdsTmtcBackend, _mode: int):
         _LOGGER.info("Mode operation hook was called")
         pass
 
@@ -108,33 +122,16 @@ def get_object_ids(self) -> ObjectIdDictT:
         return get_core_object_ids()
 
 
-class DualLogger:
-    def __init__(self, file_logger: logging.Logger, console_logger: logging.Logger):
-        self.file_logger = file_logger
-        self.console_logger = console_logger
-
-    def info(self, string: str):
-        self.console_logger.info(string)
-        self.file_logger.info(string)
-
-    def print(self, string: str):
-        print(string)
-        self.file_logger.info(string)
-
-
 class PusTmHandler(SpecificApidHandlerBase):
     def __init__(
         self,
         verif_wrapper: VerificationWrapper,
         printer: FsfwTmTcPrinter,
-        raw_logger: RawTmtcTimedLogWrapper,
     ):
         super().__init__(EXAMPLE_PUS_APID, None)
         self.printer = printer
-        self.raw_logger = raw_logger
         self.verif_wrapper = verif_wrapper
         assert self.printer.file_logger is not None
-        self.dlogger = DualLogger(self.printer.file_logger, _LOGGER)
 
     def handle_tm(self, packet: bytes, _user_args: Any):
         try:
@@ -167,17 +164,16 @@ def handle_tm(self, packet: bytes, _user_args: Any):
             dedicated_handler = True
         if service == 5:
             event_tm = Service5Tm.unpack(packet, time_reader=CdsShortTimestamp.empty())
-            self.dlogger.info(
+            _LOGGER.info(
                 f"Received event packet TM [{event_tm.service}, {event_tm.subservice}]"
             )
-            dedicated_handler = True
         if service == 17:
             ping_tm = Service17Tm.unpack(packet, time_reader=CdsShortTimestamp.empty())
             dedicated_handler = True
             if ping_tm.subservice == 2:
-                self.dlogger.info("Received Ping Reply TM[17,2]")
+                _LOGGER.info("Received Ping Reply TM[17,2]")
             else:
-                self.dlogger.info(
+                _LOGGER.info(
                     "Received Test Packet with unknown subservice"
                     f" {tm_packet.subservice}"
                 )
@@ -188,11 +184,13 @@ def handle_tm(self, packet: bytes, _user_args: Any):
             tm_packet = PusTelemetry.unpack(
                 packet, time_reader=CdsShortTimestamp.empty()
             )
-        self.raw_logger.log_tm(tm_packet)
-        if not dedicated_handler and tm_packet is not None:
-            pass
-            # TODO: Create more generic print method..
-            # self.printer.handle_long_tm_print(packet_if=tm_packet, info_if=tm_packet)
+        # TODO: Insert this into a DB instead. Maybe use sqlite for first variant.
+        # self.raw_logger.log_tm(tm_packet)
+        if not dedicated_handler:
+            _LOGGER.info(
+                f"Received PUS TM [{tm_packet.service}, {tm_packet.subservice}] with not dedicated "
+                f"handler"
+            )
 
 
 class TcHandler(TcHandlerBase):
@@ -231,23 +229,26 @@ def send_cb(self, send_params: SendCbParams):
     def queue_finished_cb(self, helper: ProcedureWrapper):
         if helper.proc_type == TcProcedureType.DEFAULT:
             def_proc = helper.to_def_procedure()
-            _LOGGER.info(
-                f"Queue handling finished for service {def_proc.service} and "
-                f"op code {def_proc.op_code}"
-            )
+            _LOGGER.info(f"Queue handling finished for command {def_proc.cmd_path}")
 
     def feed_cb(self, helper: ProcedureWrapper, wrapper: FeedWrapper):
         self.queue_helper.queue_wrapper = wrapper.queue_wrapper
         if helper.proc_type == TcProcedureType.DEFAULT:
             def_proc = helper.to_def_procedure()
-            service = def_proc.service
-            if (
-                service == CoreServiceList.SERVICE_17
-                or service == CoreServiceList.SERVICE_17_ALT
-            ):
+            cmd_path = def_proc.cmd_path
+            assert cmd_path is not None
+            # Path starts with / so the first entry of the list will be an empty string. We cut
+            # off that string.
+            cmd_path_list = cmd_path.split("/")[1:]
+            if cmd_path_list[0] == "ping":
                 return self.queue_helper.add_pus_tc(
                     PusTelecommand(service=17, subservice=1)
                 )
+            elif cmd_path_list[0] == "test":
+                if cmd_path_list[1] == "event":
+                    return self.queue_helper.add_pus_tc(
+                        PusTelecommand(service=17, subservice=128)
+                    )
 
 
 # Note about lint disable: I could split up the function but I prefer to have the whole
@@ -276,13 +277,12 @@ def main():  # noqa: C901
     # Create console logger helper and file loggers
     tmtc_logger = RegularTmtcLogWrapper()
     printer = FsfwTmTcPrinter(tmtc_logger.logger)
-    raw_logger = RawTmtcTimedLogWrapper(when=TimedLogWhen.PER_HOUR, interval=1)
     verificator = PusVerificator()
     verification_wrapper = VerificationWrapper(
         verificator, _LOGGER, printer.file_logger
     )
     # Create primary TM handler and add it to the CCSDS Packet Handler
-    tm_handler = PusTmHandler(verification_wrapper, printer, raw_logger)
+    tm_handler = PusTmHandler(verification_wrapper, printer)
     ccsds_handler = CcsdsTmHandler(generic_handler=None)
     ccsds_handler.add_apid_handler(tm_handler)
 
diff --git a/pyproject.toml b/pyproject.toml
index 758dea90..f34a7f92 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
 name = "tmtccmd"
 description = "TMTC Commander Core"
 readme = "README.md"
-version = "7.0.0"
+version = "8.0.0rc0"
 requires-python = ">=3.8"
 license = {text = "Apache-2.0 or MIT" }
 authors = [
@@ -32,8 +32,10 @@ dependencies = [
     "colorama~=0.4.0",
     "colorlog~=6.6",
     "cobs~=1.2",
-    "prompt-toolkit~=3.0",
-    "deprecation~=2.1",
+    # TODO: Use upstream again when the nested completer PR has been merged.
+    # "prompt-toolkit~=3.0",
+    "prompt-toolkit @ git+https://github.com/robamu/python-prompt-toolkit.git@nested-completer-custom-separator-based-on-latest",
+    "Deprecated~=1.2",
     "pyserial~=3.5",
     "dle-encoder~=0.2.3",
     "spacepackets~=0.21.0",
diff --git a/tmtccmd/__init__.py b/tmtccmd/__init__.py
index bf0d42e0..5c9bc9d1 100644
--- a/tmtccmd/__init__.py
+++ b/tmtccmd/__init__.py
@@ -198,7 +198,7 @@ def create_default_tmtc_backend(
             tm_handler = cast(CcsdsTmHandler, tm_handler)
     com_if = setup_wrapper.params.com_if
     if com_if is None:
-        com_if = setup_wrapper.hook_obj.assign_communication_interface(
+        com_if = setup_wrapper.hook_obj.get_communication_interface(
             setup_wrapper.params.com_if_id
         )
     tm_listener = CcsdsTmListener(tm_handler)
@@ -216,6 +216,7 @@ def create_default_tmtc_backend(
         CoreModeList.MULTI_INTERACTIVE_QUEUE_MODE
     ):
         print("-- Multi Queue Mode --")
+    assert com_if is not None
     # The global variables are set by the argument parser.
     tmtc_backend = CcsdsTmtcBackend(
         com_if=com_if,
@@ -230,13 +231,12 @@ def create_default_tmtc_backend(
         seconds=setup_wrapper.params.tc_params.delay
     )
     if init_procedure is not None:
-        tmtc_backend.current_procedure = init_procedure.base
+        tmtc_backend.current_procedure = init_procedure.procedure
     return tmtc_backend
 
 
 def setup_backend_def_procedure(
     backend: CcsdsTmtcBackend, tmtc_params: DefaultProcedureParams
 ):
-    backend.current_procedure = DefaultProcedureInfo(
-        tmtc_params.service, tmtc_params.op_code
-    )
+    assert tmtc_params.cmd_path is not None
+    backend.current_procedure = DefaultProcedureInfo(tmtc_params.cmd_path)
diff --git a/tmtccmd/cfdp/handler/dest.py b/tmtccmd/cfdp/handler/dest.py
index 7a939e8a..33f31281 100644
--- a/tmtccmd/cfdp/handler/dest.py
+++ b/tmtccmd/cfdp/handler/dest.py
@@ -7,7 +7,7 @@
 from pathlib import Path
 from typing import Deque, List, Optional, Tuple
 
-import deprecation
+from deprecated.sphinx import deprecated
 from spacepackets.cfdp import (
     ChecksumType,
     ConditionCode,
@@ -69,7 +69,6 @@
     TransactionFinishedParams,
 )
 from tmtccmd.util.countdown import Countdown
-from tmtccmd.version import get_version
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -1221,10 +1220,9 @@ def _abandon_transaction(self):
         # for this is quite easy.
         self.reset()
 
-    @deprecation.deprecated(
-        deprecated_in="6.0.0rc1",
-        current_version=get_version(),
-        details="Use insert_packet instead",
+    @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
index 737b6e2a..ac03d2d2 100644
--- a/tmtccmd/cfdp/handler/source.py
+++ b/tmtccmd/cfdp/handler/source.py
@@ -6,8 +6,7 @@
 from collections import deque
 from dataclasses import dataclass
 from typing import Deque, Optional, Tuple
-
-import deprecation
+from deprecated.sphinx import deprecated
 from spacepackets.cfdp import (
     CrcFlag,
     GenericPduPacket,
@@ -73,7 +72,6 @@
 from tmtccmd.cfdp.user import TransactionFinishedParams, TransactionParams
 from tmtccmd.util import ProvidesSeqCount
 from tmtccmd.util.countdown import Countdown
-from tmtccmd.version import get_version
 
 
 _LOGGER = logging.getLogger(__name__)
@@ -1020,10 +1018,9 @@ def _checksum_calculation(self, size_to_calculate: int) -> bytes:
             )
         return crc
 
-    @deprecation.deprecated(
-        deprecated_in="6.0.0rc1",
-        current_version=get_version(),
-        details="Use insert_packet instead",
+    @deprecated(
+        version="6.0.0rc1",
+        reason="use insert_packet instead",
     )
     def pass_packet(self, packet: AbstractFileDirectiveBase):
         self.insert_packet(packet)
diff --git a/tmtccmd/com/dummy.py b/tmtccmd/com/dummy.py
index 1b135d75..c067b378 100644
--- a/tmtccmd/com/dummy.py
+++ b/tmtccmd/com/dummy.py
@@ -2,23 +2,22 @@
 external hardware or an extra socket
 """
 from typing import Optional
-from tmtccmd.version import get_version
-import deprecation
 
-from spacepackets.ecss.pus_17_test import Service17Tm
+from deprecated.sphinx import deprecated
+from spacepackets.ccsds.time import CdsShortTimestamp
 from spacepackets.ecss.pus_1_verification import (
     RequestId,
-    VerificationParams,
     Service1Tm,
+    VerificationParams,
 )
+from spacepackets.ecss.pus_17_test import Service17Tm
 from spacepackets.ecss.tc import PusTelecommand
-from spacepackets.ccsds.time import CdsShortTimestamp
 
 from tmtccmd.com import ComInterface
 from tmtccmd.config import CoreComInterfaces
-from tmtccmd.tmtc import TelemetryListT
 from tmtccmd.pus.s1_verification import Subservice as Pus1Subservice
 from tmtccmd.pus.s17_test import Subservice as Pus17Subservice
+from tmtccmd.tmtc import TelemetryListT
 
 
 class DummyHandler:
@@ -28,10 +27,9 @@ def __init__(self):
         self.current_ssc = 0
         self.reply_pending = False
 
-    @deprecation.deprecated(
-        deprecated_in="6.0.0",
-        current_version=get_version(),
-        details="Use insert_telecommand instead",
+    @deprecated(
+        version="6.0.0",
+        reason="Use insert_telecommand instead",
     )
     def pass_telecommand(self, data: bytearray):
         self.insert_telecommand(data)
diff --git a/tmtccmd/config/__init__.py b/tmtccmd/config/__init__.py
index 346db808..50bd9506 100644
--- a/tmtccmd/config/__init__.py
+++ b/tmtccmd/config/__init__.py
@@ -36,7 +36,7 @@
     CfdpParams,
 )
 from .prompt import prompt_op_code, prompt_service
-from .tmtc import TmtcDefinitionWrapper, OpCodeEntry, OpCodeOptionBase
+from .tmtc import TmtcDefinitionWrapper, OpCodeEntry, OpCodeOptionBase, CmdTreeNode
 from .hook import HookBase
 from tmtccmd.tmtc.procedure import (
     DefaultProcedureInfo,
@@ -51,7 +51,7 @@
 _LOGGER = logging.getLogger(__name__)
 
 
-def backend_mode_conversion(mode: CoreModeList, mode_wrapper: ModeWrapper):
+def backend_mode_conversion(mode: str, mode_wrapper: ModeWrapper):
     if mode == CoreModeConverter.get_str(CoreModeList.LISTENER_MODE):
         mode_wrapper.tm_mode = TmMode.LISTENER
         mode_wrapper.tc_mode = TcMode.IDLE
@@ -115,7 +115,7 @@ def params(self):
 
 
 def tmtc_params_to_procedure(params: DefaultProcedureParams) -> DefaultProcedureInfo:
-    return DefaultProcedureInfo(service=params.service, op_code=params.op_code)
+    return DefaultProcedureInfo(cmd_path=params.cmd_path)
 
 
 def cfdp_put_req_params_to_procedure(params: CfdpParams) -> CfdpProcedureInfo:
@@ -129,9 +129,9 @@ def params_to_procedure_conversion(
 ) -> ProcedureWrapper:
     proc_wrapper = ProcedureWrapper(None)
     if param_wrapper.ptype == TcProcedureType.DEFAULT:
-        proc_wrapper.base = tmtc_params_to_procedure(param_wrapper.def_params())
+        proc_wrapper.procedure = tmtc_params_to_procedure(param_wrapper.def_params())  # type: ignore
     elif param_wrapper.ptype == TcProcedureType.CFDP:
-        proc_wrapper.base = cfdp_put_req_params_to_procedure(
-            param_wrapper.cfdp_params()
+        proc_wrapper.procedure = cfdp_put_req_params_to_procedure(
+            param_wrapper.cfdp_params()  # type: ignore
         )
     return proc_wrapper
diff --git a/tmtccmd/config/args.py b/tmtccmd/config/args.py
index f5ef72e4..28522f08 100644
--- a/tmtccmd/config/args.py
+++ b/tmtccmd/config/args.py
@@ -1,29 +1,32 @@
 """Argument parser module."""
 from __future__ import annotations
+
 import argparse
 import logging
+import os
 import sys
-from typing import Optional, List, Sequence, Union
 from dataclasses import dataclass
+from typing import List, Optional, Sequence, Tuple, Union
 
+from deprecated.sphinx import deprecated
 from prompt_toolkit.shortcuts import CompleteStyle
-
 from spacepackets.cfdp import TransmissionMode
+
+from tmtccmd.com import ComInterface
 from tmtccmd.com.utils import determine_com_if
+from tmtccmd.config.prompt import prompt_cmd_path, prompt_op_code, prompt_service
+from tmtccmd.config.tmtc import CmdTreeNode
 from tmtccmd.tmtc.procedure import TcProcedureType
-from tmtccmd.config.prompt import prompt_op_code, prompt_service
-from tmtccmd.com import ComInterface
 
 from .defs import (
-    CoreModeList,
+    CfdpParams,
     CoreComInterfaces,
     CoreModeConverter,
+    CoreModeList,
     DefaultProcedureParams,
-    CfdpParams,
 )
 from .hook import HookBase
 
-
 _LOGGER = logging.getLogger(__name__)
 
 
@@ -97,6 +100,9 @@ class AppParams:
     use_gui: bool = False
     reduced_printout: bool = False
     use_ansi_colors: bool = True
+    print_tree: bool = False
+    tree_print_with_description: bool = True
+    tree_print_max_depth: Optional[int] = None
     compl_style: CompleteStyle = CompleteStyle.READLINE_LIKE
 
 
@@ -162,7 +168,7 @@ def parse_default_tmtccmd_input_arguments(
     parser: argparse.ArgumentParser,
     print_known_args: bool = False,
     print_unknown_args: bool = False,
-) -> (argparse.Namespace, List[str]):
+) -> Tuple[argparse.Namespace, List[str]]:
     """Parses all input arguments by calling :py:meth:`argparse.ArgumentParser.parse_known_args`.
     It is recommended to use the :py:class:`PreArgsParsingWrapper` instead of using this function
     directly.
@@ -195,16 +201,22 @@ def parse_default_tmtccmd_input_arguments(
 
 def add_default_procedure_arguments(parser_or_subparser: argparse.ArgumentParser):
     parser_or_subparser.add_argument(
-        "-s",
-        "--service",
-        help="Procedure service code, used for the default procedure mode",
+        "-p",
+        "--path",
+        dest="cmd_path",
+        help="Command tree path, used to uniquely identify command or command stack to be sent.",
         default=None,
     )
     parser_or_subparser.add_argument(
-        "-o",
-        "--op_code",
-        help="Procedure operation code, used for the default procedure mode",
-        default=None,
+        "-T",
+        "--pt",
+        "--print-tree",
+        dest="print_tree",
+        nargs="*",
+        help=(
+            f"Optional arguments [b] [<numMaxDepth>]. Print the command definition tree. You can"
+            f"{os.linesep}optionally add b to omit descriptions, and a maximum print depth."
+        ),
     )
     add_tmtc_mode_arguments(parser_or_subparser)
     add_tmtc_listener_arg(parser_or_subparser)
@@ -224,18 +236,18 @@ def add_cfdp_procedure_arguments(parser_or_subparser: argparse.ArgumentParser):
         "--proxy",
         action="store_true",
         help=(
-            "Used to trigger a proxy operation at the remote CFDP entity.\nMost"
-            " commonly used to request a file from the remote entity.\nPlease note that"
-            " this inverses the meaning of the destination and file parameter."
+            f"Used to trigger a proxy operation at the remote CFDP entity.{os.linesep}Most"
+            f" commonly used to request a file from the remote entity.{os.linesep}Please note that"
+            f" this inverses the meaning of the destination and file parameter."
         ),
     )
     parser_or_subparser.add_argument(
         "-t",
         "--type",
         help=(
-            "Specify the transfer type\n"
-            ' - "0" or "ack" for unacknowledged (Class 0) transfers\n'
-            ' - "1" or "nak" for acknowledged (Class 1) transfers. Default value'
+            f"Specify the transfer type{os.linesep}"
+            f' - "0" or "ack" for unacknowledged (Class 0) transfers{os.linesep}'
+            f' - "1" or "nak" for acknowledged (Class 1) transfers. Default value'
         ),
         default="nak",
     )
@@ -275,7 +287,7 @@ def add_generic_arguments(arg_parser: argparse.ArgumentParser):
 
 
 def add_tmtc_mode_arguments(arg_parser: argparse.ArgumentParser):
-    from tmtccmd.config import CoreModeList, CoreModeConverter
+    from tmtccmd.config import CoreModeConverter, CoreModeList
 
     help_text = (
         "Core Modes. Default:"
@@ -323,7 +335,7 @@ def add_tmtc_listener_arg(arg_parser: argparse.ArgumentParser):
         "--prompt-proc",
         help=(
             "If this is specified in addition to the listener mode flag -l, the"
-            " commander will\ntry to determine a service and operation code"
+            " commander will\nprompt for the next command or procedure to be sent."
         ),
         dest="prompt_proc",
         action="store_true",
@@ -357,6 +369,10 @@ def add_ethernet_arguments(arg_parser: argparse.ArgumentParser):
     )
 
 
+@deprecated(
+    version="8.0.0",
+    reason="use determine_cmd_path instead",
+)
 def find_service_and_op_code(
     params: SetupParams,
     def_params: DefaultProcedureParams,
@@ -385,6 +401,27 @@ def find_service_and_op_code(
         def_params.op_code = pargs.op_code
 
 
+def determine_cmd_path(
+    params: SetupParams,
+    def_params: DefaultProcedureParams,
+    hook_obj: HookBase,
+    pargs: argparse.Namespace,
+    use_prompts: bool,
+):
+    cmd_defs = hook_obj.get_command_definitions()
+    if pargs.cmd_path is None:
+        if use_prompts:
+            print("No command path (-p) argument specified, prompting from user")
+            # Try to get the service list from the hook base and prompt service
+            # from user
+            def_params.cmd_path = prompt_cmd_path(
+                cmd_defs, params.app_params.compl_style
+            )
+            print(f"Using command path: {def_params.cmd_path}")
+    else:
+        def_params.cmd_path = pargs.cmd_path
+
+
 def args_to_params_generic(
     pargs: argparse.Namespace,
     params: SetupParams,
@@ -396,7 +433,17 @@ def args_to_params_generic(
         params.app_params.use_gui = False
     else:
         params.app_params.use_gui = pargs.gui
+    if pargs.print_tree is not None:
+        params.app_params.print_tree = True
+        for arg in pargs.print_tree:
+            if "b" in arg:
+                params.app_params.tree_print_with_description = False
+            if arg.isdigit():
+                params.app_params.tree_print_max_depth = int(arg)
+    else:
+        params.app_params.print_tree = False
     if pargs.com_if is None or pargs.com_if == CoreComInterfaces.UNSPECIFIED.value:
+        assert hook_obj.cfg_path is not None
         params.com_if_id = determine_com_if(
             hook_obj.get_com_if_dict(), hook_obj.cfg_path, use_prompts
         )
@@ -461,7 +508,7 @@ def args_to_all_params_tmtc(
     def_tmtc_params: DefaultProcedureParams,
     hook_obj: HookBase,
     use_prompts: bool,
-    assign_com_if: bool,
+    assign_com_if: bool = True,
 ):
     """This function converts command line arguments to the internalized setup parameters.
 
@@ -470,8 +517,8 @@ def args_to_all_params_tmtc(
 
     If some arguments are unspecified, they are set here with (variable) default values.
 
-    :param pargs: Parsed arguments from calling parse method
-    :param params: Setup parameter object which will be set by this function
+    :param pargs: Parsed arguments from calling parse method.
+    :param params: Setup parameter object which will be set by this function.
     :param hook_obj:
     :param def_tmtc_params:
     :param use_prompts: Specify whether terminal prompts are allowed to retrieve unspecified
@@ -496,7 +543,7 @@ def args_to_all_params_tmtc(
         params.mode = pargs.mode
     if (
         params.backend_params.listener
-        and (not pargs.service and not pargs.op_code)
+        and (not pargs.cmd_path)
         and not mode_set_explicitely
         and (not pargs.prompt_proc)
     ):
@@ -510,18 +557,33 @@ def args_to_all_params_tmtc(
             params.tc_params.delay = 0.0
     else:
         params.tc_params.delay = float(pargs.delay)
-    tmtc_defs = hook_obj.get_tmtc_definitions()
-    if tmtc_defs is None:
-        _LOGGER.warning("Invalid Service to Op-Code dictionary detected")
+    if (
+        params.mode != CoreModeConverter.get_str(CoreModeList.LISTENER_MODE)
+        and not params.app_params.print_tree
+    ):
+        determine_cmd_path(
+            params=params,
+            hook_obj=hook_obj,
+            use_prompts=use_prompts,
+            pargs=pargs,
+            def_params=def_tmtc_params,
+        )
+
+
+def perform_tree_printout(app_params: AppParams, cmd_def_tree: CmdTreeNode):
+    if app_params.tree_print_with_description:
+        info_str = "with full descriptions"
     else:
-        if params.mode != CoreModeConverter.get_str(CoreModeList.LISTENER_MODE):
-            find_service_and_op_code(
-                params=params,
-                hook_obj=hook_obj,
-                use_prompts=use_prompts,
-                pargs=pargs,
-                def_params=def_tmtc_params,
-            )
+        info_str = "without descriptions"
+    if app_params.tree_print_max_depth is not None:
+        info_str += f" and maximum depth {app_params.tree_print_max_depth}"
+    print(f"Printing command tree {info_str}:")
+    print(
+        cmd_def_tree.str_for_tree(
+            app_params.tree_print_with_description,
+            app_params.tree_print_max_depth,
+        )
+    )
 
 
 class PreArgsParsingWrapper:
@@ -596,6 +658,7 @@ def parse(
                 patched_args.extend(sys.argv[1:])
         if patched_args is None:
             patched_args = sys.argv[1:]
+        assert self.args_parser is not None
         args_raw, unknown_args = parse_default_tmtccmd_input_arguments(
             args=patched_args,
             parser=self.args_parser,
@@ -613,11 +676,13 @@ def add_def_proc_args(self):
         """Add the default tmtc procedure parameters to the default parser. This includes
         the service and operation code flags."""
         self._check_arg_parser()
+        assert self.args_parser is not None
         add_default_procedure_arguments(self.args_parser)
 
     def add_cfdp_args(self):
         """Add the default CFDP procedure parameters to the default parser."""
         self._check_arg_parser()
+        assert self.args_parser is not None
         add_cfdp_procedure_arguments(self.args_parser)
 
     def _check_arg_parser(self):
@@ -629,7 +694,7 @@ def _check_arg_parser(self):
 
     def add_def_proc_and_cfdp_as_subparsers(
         self,
-    ) -> (argparse.ArgumentParser, argparse.ArgumentParser):
+    ) -> Tuple[argparse.ArgumentParser, argparse.ArgumentParser]:
         """Add the default tmtc and cfdp procedure as subparsers."""
         self._monkey_patch_missing_subparser = True
         subparser = self.args_parser.add_subparsers(dest="proc_type")
@@ -686,7 +751,7 @@ def __init__(
         self.params = params
         self.unknown_args = unknown_args
         self.hook_obj = hook_obj
-        self.assign_com_if = True
+        self.assign_com_if_on_conversion_if_applicable = True
 
     @property
     def use_gui(self):
@@ -716,7 +781,7 @@ def set_params_without_prompts(self, proc_wrapper: ProcedureParamsWrapper):
     def _set_params(self, proc_base: ProcedureParamsWrapper, with_prompts: bool):
         param_type = self.request_type_from_args()
         if param_type == TcProcedureType.DEFAULT:
-            def_proc_params = DefaultProcedureParams()
+            def_proc_params = DefaultProcedureParams(None)
             if with_prompts:
                 self.set_tmtc_params_with_prompts(def_proc_params)
             else:
@@ -759,7 +824,7 @@ def _set_tmtc_params(
                 hook_obj=self.hook_obj,
                 use_prompts=use_prompts,
                 def_tmtc_params=def_tmtc_params,
-                assign_com_if=self.assign_com_if,
+                assign_com_if=self.assign_com_if_on_conversion_if_applicable,
             )
         except KeyboardInterrupt:
             raise KeyboardInterrupt(
@@ -774,7 +839,7 @@ def _set_cfdp_params(self, cfdp_params: CfdpParams, use_prompts: bool):
                 params=self.params,
                 hook_obj=self.hook_obj,
                 use_prompts=use_prompts,
-                assign_com_if=self.assign_com_if,
+                assign_com_if=self.assign_com_if_on_conversion_if_applicable,
             )
         except KeyboardInterrupt:
             raise KeyboardInterrupt(
diff --git a/tmtccmd/config/defs.py b/tmtccmd/config/defs.py
index 750103fe..8ebd0ae9 100644
--- a/tmtccmd/config/defs.py
+++ b/tmtccmd/config/defs.py
@@ -11,8 +11,7 @@
 
 @dataclass
 class DefaultProcedureParams:
-    service: Optional[str] = None
-    op_code: Optional[str] = None
+    cmd_path: Optional[str]
 
 
 @dataclass
@@ -43,7 +42,7 @@ class CoreComInterfaces(str, enum.Enum):
     UNSPECIFIED = "unspec"
 
 
-CORE_COM_IF_DICT = {
+CORE_COM_IF_DICT: ComIfDictT = {
     CoreComInterfaces.DUMMY: ("Dummy Interface", None),
     CoreComInterfaces.UDP: ("TCP/IP with UDP datagrams", None),
     CoreComInterfaces.TCP: ("TCP/IP with TCP", None),
diff --git a/tmtccmd/config/globals.py b/tmtccmd/config/globals.py
index 61aef2ed..b26b60e4 100644
--- a/tmtccmd/config/globals.py
+++ b/tmtccmd/config/globals.py
@@ -3,7 +3,7 @@
 import logging
 import pprint
 
-import deprecation
+from deprecated.sphinx import deprecated
 from spacepackets.ecss.conf import (
     set_default_tc_apid,
     set_default_tm_apid,
@@ -18,7 +18,6 @@
     ComIfDictT,
 )
 from tmtccmd.config.tmtc import TmtcDefinitionWrapper
-from tmtccmd.version import get_version
 
 DEF_WRAPPER = None
 
@@ -64,28 +63,28 @@ class CoreGlobalIds(enum.IntEnum):
     END = 300
 
 
-@deprecation.deprecated(deprecated_in="6.0.0rc0", current_version=get_version())
+@deprecated(version="6.0.0rc0", reason="globals module deprecated")
 def set_json_cfg_path(json_cfg_path: str):
     update_global(CoreGlobalIds.JSON_CFG_PATH, json_cfg_path)
 
 
-@deprecation.deprecated(deprecated_in="6.0.0rc0", current_version=get_version())
+@deprecated(version="6.0.0rc0", reason="globals module deprecated")
 def get_json_cfg_path() -> str:
     return get_global(CoreGlobalIds.JSON_CFG_PATH)
 
 
-@deprecation.deprecated(deprecated_in="6.0.0rc0", current_version=get_version())
+@deprecated(version="6.0.0rc0", reason="globals module deprecated")
 def set_glob_com_if_dict(custom_com_if_dict: ComIfDictT):
     CORE_COM_IF_DICT.update(custom_com_if_dict)
     update_global(CoreGlobalIds.COM_IF_DICT, CORE_COM_IF_DICT)
 
 
-@deprecation.deprecated(deprecated_in="6.0.0rc0", current_version=get_version())
+@deprecated(version="6.0.0rc0", reason="globals module deprecated")
 def get_glob_com_if_dict() -> ComIfDictT:
     return get_global(CoreGlobalIds.COM_IF_DICT)
 
 
-@deprecation.deprecated(deprecated_in="6.0.0rc0", current_version=get_version())
+@deprecated(version="6.0.0rc0", reason="globals module deprecated")
 def set_default_globals_pre_args_parsing(
     apid: int,
     com_if_id: str = CoreComInterfaces.DUMMY.value,
@@ -120,7 +119,7 @@ def set_default_globals_pre_args_parsing(
     update_global(CoreGlobalIds.MODE, CoreModeList.LISTENER_MODE)
 
 
-@deprecation.deprecated(deprecated_in="6.0.0rc0", current_version=get_version())
+@deprecated(version="6.0.0rc0", reason="globals module deprecated")
 def check_and_set_other_args(args):
     if args.listener is not None:
         update_global(CoreGlobalIds.USE_LISTENER_AFTER_OP, args.listener)
@@ -139,6 +138,7 @@ def check_and_set_other_args(args):
     update_global(CoreGlobalIds.TC_SEND_TIMEOUT_FACTOR, 3)
 
 
+@deprecated(version="8.0.0", reason="globals module deprecated")
 def check_and_set_core_service_arg(
     service_arg: any, custom_service_list: collections.abc.Iterable = None
 ):
@@ -175,6 +175,7 @@ def check_and_set_core_service_arg(
     update_global(CoreGlobalIds.CURRENT_SERVICE, service_value)
 
 
+@deprecated(version="8.0.0", reason="use command tree API instead")
 def get_default_tmtc_defs() -> TmtcDefinitionWrapper:
     global DEF_WRAPPER
     if DEF_WRAPPER is None:
diff --git a/tmtccmd/config/hook.py b/tmtccmd/config/hook.py
index e90fb8f0..f5c21a08 100644
--- a/tmtccmd/config/hook.py
+++ b/tmtccmd/config/hook.py
@@ -1,16 +1,21 @@
+from deprecated.sphinx import deprecated
+import logging
 from typing import Optional
 from abc import abstractmethod, ABC
 
 from tmtccmd.util.obj_id import ObjectIdDictT
 
+from tmtccmd.config.tmtc import CmdTreeNode
 from tmtccmd.core import BackendBase
-from tmtccmd.util.retval import RetvalDictT
 
 from .com import ComCfgBase, ComInterface
 from .tmtc import TmtcDefinitionWrapper
 from .defs import default_json_path, CORE_COM_IF_DICT, ComIfDictT
 
 
+_LOGGER = logging.getLogger(__name__)
+
+
 class HookBase(ABC):
     """This hook allows users to adapt the TMTC commander core to the unique mission requirements.
     It is used by implementing all abstract functions and then passing the instance to the
@@ -32,22 +37,33 @@ def get_object_ids(self) -> ObjectIdDictT:
         """
         return get_core_object_ids()
 
-    @abstractmethod
+    @deprecated(
+        version="8.0.0rc0",
+        reason="implement and use get_communication_interface instead",
+    )
     def assign_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
         """Assign the communication interface used by the TMTC commander to send and receive
         TMTC with.
 
         :param com_if_key:      String key of the communication interface to be created.
         """
+        return self.get_communication_interface(com_if_key)
+
+    @abstractmethod
+    def get_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
         from tmtccmd.config.com import create_com_interface_default
 
+        assert self.cfg_path is not None
         cfg_base = ComCfgBase(com_if_key=com_if_key, json_cfg_path=self.cfg_path)
         return create_com_interface_default(cfg_base)
 
     def get_com_if_dict(self) -> ComIfDictT:
         return CORE_COM_IF_DICT
 
-    @abstractmethod
+    @deprecated(
+        version="8.0.0rc0",
+        reason="implement and use get_command_definitions instead",
+    )
     def get_tmtc_definitions(self) -> TmtcDefinitionWrapper:
         """This is a dicitonary mapping services represented by strings to an operation code
         dictionary.
@@ -58,6 +74,11 @@ def get_tmtc_definitions(self) -> TmtcDefinitionWrapper:
 
         return get_default_tmtc_defs()
 
+    @abstractmethod
+    def get_command_definitions(self) -> CmdTreeNode:
+        """This function should return the root node of the command definition tree."""
+        pass
+
     def perform_mode_operation(self, tmtc_backend: BackendBase, mode: int):
         """Perform custom mode operations.
 
@@ -65,11 +86,4 @@ def perform_mode_operation(self, tmtc_backend: BackendBase, mode: int):
         :param mode:
         :return:
         """
-        print("No custom mode operation implemented")
-
-    def get_retval_dict(self) -> RetvalDictT:
-        from tmtccmd import get_console_logger
-
-        logger = get_console_logger()
-        logger.info("No return value dictionary specified")
-        return dict()
+        _LOGGER.warning("No custom mode operation implemented")
diff --git a/tmtccmd/config/prompt.py b/tmtccmd/config/prompt.py
index 9b45c62d..ff889fd4 100644
--- a/tmtccmd/config/prompt.py
+++ b/tmtccmd/config/prompt.py
@@ -1,13 +1,20 @@
+import os
 import logging
 
-from prompt_toolkit.completion import WordCompleter
-from prompt_toolkit.shortcuts import CompleteStyle
 import prompt_toolkit
-from tmtccmd.config.tmtc import OpCodeEntry, TmtcDefinitionWrapper
+from deprecated.sphinx import deprecated
+from prompt_toolkit.completion import NestedCompleter, WordCompleter
+from prompt_toolkit.shortcuts import CompleteStyle
+
+from tmtccmd.config.tmtc import CmdTreeNode, OpCodeEntry, TmtcDefinitionWrapper
 
 _LOGGER = logging.getLogger(__name__)
 
 
+@deprecated(
+    reason="use prompt_cmd_path instead",
+    version="8.0.0",
+)
 def prompt_service(
     tmtc_defs: TmtcDefinitionWrapper,
     compl_style: CompleteStyle = CompleteStyle.READLINE_LIKE,
@@ -46,6 +53,49 @@ def prompt_service(
             _LOGGER.warning("Invalid key, try again")
 
 
+def prompt_cmd_path(
+    cmd_def_tree: CmdTreeNode, compl_style: CompleteStyle = CompleteStyle.READLINE_LIKE
+) -> str:
+    compl_dict = cmd_def_tree.name_dict
+    compl_dict = compl_dict.get("/")
+    if compl_dict is None:
+        return "/"
+    compl_dict.update({":p": None})
+    compl_dict.update({":fp": None})
+    nested_completer = NestedCompleter.from_nested_dict(compl_dict, separator="/")
+    help_txt = (
+        f"Additional commands for prompt:{os.linesep}"
+        f":p Tree Print | :pf Full Print | :r Retry | :h Help Text {os.linesep}"
+        f"Auto complete is available using Tab after typing the slash character."
+    )
+    print(help_txt)
+    while True:
+        path_or_cmd = prompt_toolkit.prompt(
+            message="> ",
+            completer=nested_completer,
+            complete_style=compl_style,
+        )
+        if path_or_cmd == ":p":
+            print(cmd_def_tree.str_for_tree(False))
+            continue
+        elif path_or_cmd == ":pf":
+            print(cmd_def_tree.str_for_tree(True))
+            continue
+        elif ":h" in path_or_cmd:
+            print(help_txt)
+            continue
+        elif ":r" in path_or_cmd:
+            continue
+        if not cmd_def_tree.contains_path(f"/{path_or_cmd}"):
+            yes_or_no = input(
+                "Command definitions tree does not contain the path. Try again? [y/n]: "
+            )
+            if yes_or_no in ["y", "yes", "1"]:
+                continue
+        break
+    return f"/{path_or_cmd}"
+
+
 def build_service_word_completer(
     tmtc_defs: TmtcDefinitionWrapper,
 ) -> WordCompleter:
diff --git a/tmtccmd/config/tmtc.py b/tmtccmd/config/tmtc.py
index 717c3d06..12c19fe2 100644
--- a/tmtccmd/config/tmtc.py
+++ b/tmtccmd/config/tmtc.py
@@ -1,4 +1,167 @@
-from typing import Union, List, Optional, Dict, Tuple
+from __future__ import annotations
+
+import enum
+import os
+from typing import Any, Dict, List, Optional, Set, Tuple, Union
+
+from deprecated.sphinx import deprecated
+
+
+class TreePart(enum.Enum):
+    EDGE = "├──"
+    LINE = "│  "
+    CORNER = "└──"
+    BLANK = "   "
+
+
+class DepthInfo:
+    def __init__(
+        self,
+        depth: int,
+        last_child: bool,
+        max_depth: Optional[int] = None,
+        layer_is_last_set: Optional[Set[int]] = None,
+    ) -> None:
+        self.depth = depth
+        self.last_child = last_child
+        if layer_is_last_set is None:
+            self.layer_is_last_set = set()
+        else:
+            self.layer_is_last_set = layer_is_last_set
+        self.max_depth = max_depth
+
+    def is_layer_for_last_child(self, depth: int) -> bool:
+        return depth in self.layer_is_last_set
+
+    def set_layer_is_last_child(self, depth: int):
+        self.layer_is_last_set.add(depth)
+
+
+class CmdTreeNode:
+    def __init__(
+        self, name: str, description: str, parent: Optional[CmdTreeNode] = None
+    ) -> None:
+        self.name = name
+        self.description = description
+        self.parent: Optional[CmdTreeNode] = parent
+        self.children: Dict[str, CmdTreeNode] = {}
+
+    @classmethod
+    def root_node(cls) -> CmdTreeNode:
+        return cls(name="/", description="Root Node", parent=None)
+
+    def __getitem__(self, arg):
+        return self.children[arg]
+
+    def add_child(self, child: CmdTreeNode):
+        child.parent = self
+        self.children.update({child.name: child})
+
+    def contains_path(self, path: str) -> bool:
+        """Check whether a full slash separated command path is contained within
+        the command tree."""
+        return self.contains_path_from_node_list(path.split("/"))
+
+    def contains_path_from_node_list(self, node_name_list: List[str]) -> bool:
+        """Check whether the given list of nodes are contained within the command tree."""
+        if len(node_name_list) == 0:
+            return False
+        # Only root node.
+        if len(node_name_list) == 2 and node_name_list == ["", ""]:
+            return True
+        if node_name_list[0] == "":
+            # Cut off empty string left of root node /
+            node_name_list = node_name_list[1:]
+        for child in self.children.values():
+            if node_name_list[0] == child.name:
+                # This is the last node.
+                if len(node_name_list) == 1:
+                    return True
+                return child.contains_path_from_node_list(node_name_list[1:])
+        return False
+
+    @property
+    def name_dict(self) -> Dict[str, Optional[Dict[str, Any]]]:
+        """Returns a nested dictionary where the key is always the name of the node, and the
+        value is one nested name dictionary for each child node."""
+        children_dict = {}
+        if self.children:
+            for child in self.children.values():
+                children_dict.update(child.name_dict)
+            return {self.name: children_dict}
+        return {self.name: None}
+
+    def str_for_tree(
+        self, with_description: bool, max_depth: Optional[int] = None
+    ) -> str:
+        return self.__str_for_depth(
+            with_description, DepthInfo(depth=0, last_child=False, max_depth=max_depth)
+        )
+
+    def __str_for_depth(self, with_description: bool, depth_info: DepthInfo) -> str:
+        line = ""
+        # If we are at a larger depth than 0, we want to prepend the name using a special
+        # format. Example:
+        #
+        # Example 1:
+        # /
+        # ├── ACS
+        # │  └── ACS_CTRL
+        # └── TCS
+        #
+        # Example 2:
+        # /
+        # ├── ACS
+        # │  └── ACS_CTRL
+        # └── TCS
+        #    └── TCS_CTRL
+        #
+        # Example 3:
+        # /
+        # ├── ACS
+        # │  ├── ACS_CTRL
+        # │  └── MGM_0
+        # │     └── UPDATE_CFG
+        # ├── TCS
+        # │  ├── TCS_CTRL
+        # │  └── PT1000_0
+        # └── PING
+        for i in range(depth_info.depth):
+            if i == depth_info.depth - 1:
+                if depth_info.last_child:
+                    line += TreePart.CORNER.value + " "
+                else:
+                    line += TreePart.EDGE.value + " "
+            else:
+                if depth_info.is_layer_for_last_child(i):
+                    line += TreePart.BLANK.value
+                else:
+                    line += TreePart.LINE.value
+        if depth_info.max_depth is not None and depth_info.depth > depth_info.max_depth:
+            line += f"... (cut-off, maximum depth {depth_info.max_depth}){os.linesep}"
+            return line
+        else:
+            line += self.name
+            if with_description:
+                line = f"{line.ljust(35)} [ " + self.description + " ]"
+        string = line + os.linesep
+        for idx, child in enumerate(self.children.values()):
+            last_child = True if (idx == (len(self.children) - 1)) else False
+            child_depth_info = DepthInfo(
+                depth=depth_info.depth + 1,
+                last_child=last_child,
+                max_depth=depth_info.max_depth,
+                layer_is_last_set=depth_info.layer_is_last_set,
+            )
+            if last_child:
+                child_depth_info.set_layer_is_last_child(depth_info.depth)
+            # Use recursion here to get the string for the subtree.
+            string += child.__str_for_depth(with_description, child_depth_info)
+        return string
+
+    def __str__(self) -> str:
+        return self.str_for_tree(False)
+
 
 ServiceNameT = str
 ServiceInfoT = str
@@ -6,6 +169,10 @@
 OpCodeInfoT = str
 
 
+@deprecated(
+    version="8.0.0",
+    reason="use the new command definition tree instead",
+)
 class OpCodeOptionBase:
     def __init__(self):
         pass
@@ -14,6 +181,10 @@ def __init__(self):
 OpCodeDict = Dict[str, Tuple[OpCodeInfoT, OpCodeOptionBase]]
 
 
+@deprecated(
+    version="8.0.0",
+    reason="use the new command definition tree instead",
+)
 class OpCodeEntry:
     def __init__(self):
         self._op_code_dict_num_keys: OpCodeDict = dict()
@@ -74,6 +245,10 @@ def op_code_dict_str_keys(self):
 ServiceOpCodeDictT = Dict[ServiceNameT, ServiceDictValueT]
 
 
+@deprecated(
+    version="8.0.0",
+    reason="use the new command definition tree instead",
+)
 class TmtcDefinitionWrapper:
     def __init__(self, init_defs: Optional[ServiceOpCodeDictT] = None):
         if init_defs is None:
@@ -105,6 +280,10 @@ def sort(self):
 REGISTER_CBS = set()
 
 
+@deprecated(
+    version="8.0.0",
+    reason="use the new command definition tree instead",
+)
 def tmtc_definitions_provider(adder_func):
     """Function decorator which registers the decorated function to be a TMTC definition provider.
     The :py:func:`execute_tmtc_def_functions` function can be used to call all functions.
@@ -125,6 +304,10 @@ def call_explicitely(defs: TmtcDefinitionWrapper, *args, **kwargs):
     return call_explicitely
 
 
+@deprecated(
+    version="8.0.0",
+    reason="use the new command definition tree instead",
+)
 def call_all_definitions_providers(defs: TmtcDefinitionWrapper, *args, **kwargs):
     global REGISTER_CBS
     if REGISTER_CBS:
diff --git a/tmtccmd/core/base.py b/tmtccmd/core/base.py
index d3516daf..a0639998 100644
--- a/tmtccmd/core/base.py
+++ b/tmtccmd/core/base.py
@@ -1,10 +1,11 @@
+from typing import Any
 import enum
 from abc import abstractmethod
 
 
 class FrontendBase:
     @abstractmethod
-    def start(self, args: any):
+    def start(self, args: Any):
         """
         Start the frontend.
         :return:
diff --git a/tmtccmd/gui/buttons.py b/tmtccmd/gui/buttons.py
index 793f61c9..d57fa23e 100644
--- a/tmtccmd/gui/buttons.py
+++ b/tmtccmd/gui/buttons.py
@@ -4,11 +4,11 @@
 from PyQt6.QtCore import QThreadPool, QRunnable
 from PyQt6.QtWidgets import QPushButton
 
-from tmtccmd import HookBase, DefaultProcedureInfo
+from tmtccmd import HookBase
 from tmtccmd.gui.defs import (
     SharedArgs,
     LocalArgs,
-    WorkerOperationsCodes,
+    WorkerOperationsCode,
     DISCONNECT_BTTN_STYLE,
     CONNECT_BTTN_STYLE,
     COMMAND_BUTTON_STYLE,
@@ -70,15 +70,10 @@ def _connect_button_pressed(self):
         if self._args.state.current_com_if != self._args.state.last_com_if:
             self._com_if_needs_switch = True
         self.button.setEnabled(False)
-        worker = FrontendWorker(
-            LocalArgs(
-                WorkerOperationsCodes.OPEN_COM_IF,
-                (
-                    self._com_if_needs_switch,
-                    self._args.state.current_com_if,
-                    self._bttn_params.hook_obj,
-                ),
-            ),
+        worker = FrontendWorker.spawn_for_opening_com_if(
+            self._com_if_needs_switch,
+            self._args.state.current_com_if,
+            self._bttn_params.hook_obj,
             self._args.shared,
         )
         self._next_con_state = True
@@ -111,7 +106,7 @@ def _disconnect_button_pressed(self):
         self.button.setEnabled(False)
         self._next_con_state = False
         worker = FrontendWorker(
-            LocalArgs(WorkerOperationsCodes.CLOSE_COM_IF, None), self._args.shared
+            LocalArgs(WorkerOperationsCode.CLOSE_COM_IF, None), self._args.shared
         )
         worker.signals.finished.connect(self._button_op_done)
         self._args.pool.start(worker)
@@ -151,7 +146,7 @@ def abort_thread(self):
     def start_listener(self):
         LOGGER.info("Starting TM listener")
         self.worker = FrontendWorker(
-            LocalArgs(WorkerOperationsCodes.LISTEN_FOR_TM, 0.4), self.args.shared
+            LocalArgs(WorkerOperationsCode.LISTEN_FOR_TM, 0.4), self.args.shared
         )
         self._next_listener_state = True
         self._conn_button.setDisabled(True)
@@ -161,8 +156,9 @@ def start_listener(self):
     def stop_listener(self):
         LOGGER.info("Stopping TM listener")
         self._next_listener_state = False
-        self.worker.signals.finished.connect(self.button_op_done)
-        self.worker.signals.stop.emit(None)
+        if self.worker is not None:
+            self.worker.signals.finished.connect(self.button_op_done)
+            self.worker.signals.stop.emit(None)
         self.button.setEnabled(False)
 
     def button_op(self):
@@ -201,11 +197,10 @@ def _button_op(self):
         if self.debug_mode:
             LOGGER.info("Send command button pressed.")
         self.button.setDisabled(True)
-        self._args.shared.backend.current_procedure = DefaultProcedureInfo(
-            self._args.state.current_service, self._args.state.current_op_code
-        )
-        worker = FrontendWorker(
-            LocalArgs(WorkerOperationsCodes.ONE_QUEUE_MODE, None), self._args.shared
+        if self._args.state.current_cmd_path is None:
+            return
+        worker = FrontendWorker.spawn_for_cmd_path(
+            self._args.state.current_cmd_path, self._args.shared
         )
         worker.signals.finished.connect(self._finish_op)
         self._args.pool.start(worker)
diff --git a/tmtccmd/gui/cmd_select.py b/tmtccmd/gui/cmd_select.py
new file mode 100644
index 00000000..a2a764a5
--- /dev/null
+++ b/tmtccmd/gui/cmd_select.py
@@ -0,0 +1,124 @@
+from PyQt6.QtCore import pyqtSignal
+from PyQt6.QtGui import QStandardItem, QStandardItemModel
+from PyQt6.QtWidgets import (
+    QApplication,
+    QHBoxLayout,
+    QLabel,
+    QPushButton,
+    QTreeView,
+    QVBoxLayout,
+    QWidget,
+)
+
+from tmtccmd.config.tmtc import CmdTreeNode
+
+
+class CommandPathSelectWidget(QWidget):
+    NODE_NAME_COLUMN = 0
+    DESCRIPTION_COLUMN = 1
+
+    path_selected_sig = pyqtSignal(str)
+    closed = pyqtSignal()
+
+    def __init__(self, root_node: CmdTreeNode):
+        super().__init__()
+        self.root_node = root_node
+        self.currently_selected_path = None
+        self.init_ui()
+
+    def init_ui(self):
+        self.setWindowTitle("Command path selector")
+
+        self.tree_model = QStandardItemModel()
+        self.tree_model.setHorizontalHeaderLabels(["Node Name", "Description"])
+        parent_item = self.tree_model.invisibleRootItem()
+        assert parent_item is not None
+        CommandPathSelectWidget.build_tree_model_recursively(
+            parent_item, self.root_node
+        )
+
+        self.tree_view = QTreeView()
+        self.tree_view.setModel(self.tree_model)
+        self.tree_view.expanded.connect(self.on_item_expanded)
+
+        self.expand_full_button = QPushButton("Expand All")
+        self.expand_full_button.clicked.connect(self.expand_all_items)
+
+        self.fold_all_button = QPushButton("Fold All")
+        self.fold_all_button.clicked.connect(self.collapse_all_items)
+
+        button_layout = QHBoxLayout()
+        button_layout.addWidget(self.expand_full_button)
+        button_layout.addWidget(self.fold_all_button)
+
+        self.select_path_button = QPushButton("Select Current Path")
+        self.select_path_button.clicked.connect(self.select_path_clicked)
+        self.copy_path_button = QPushButton("Copy Current Path to Clipboard")
+        self.copy_path_button.clicked.connect(self.copy_path_clicked)
+        self.path_confirmed_button = QPushButton("Confirm Selected Path")
+        self.path_confirmed_button.clicked.connect(self.path_confirmed_clicked)
+
+        self.path_label = QLabel()
+
+        layout = QVBoxLayout()
+        layout.addLayout(button_layout)
+        layout.addWidget(self.tree_view)
+        layout.addWidget(self.select_path_button)
+        layout.addWidget(self.copy_path_button)
+        layout.addWidget(self.path_label)
+        layout.addWidget(self.path_confirmed_button)
+        self.setLayout(layout)
+
+    def expand_all_items(self):
+        self.tree_view.expandAll()
+
+    def collapse_all_items(self):
+        self.tree_view.collapseAll()
+
+    def path_confirmed_clicked(self):
+        if self.currently_selected_path is None:
+            # TODO: Error handling in some shape or form, maybe message box?
+            return
+        self.path_selected_sig.emit(self.currently_selected_path)
+        self.closed.emit()
+        self.close()
+
+    @staticmethod
+    def build_tree_model_recursively(
+        current_item: QStandardItem, current_node: CmdTreeNode
+    ):
+        for child in current_node.children.values():
+            new_item = QStandardItem(child.name)
+            current_item.appendRow([new_item, QStandardItem(child.description)])
+            if child.children:
+                CommandPathSelectWidget.build_tree_model_recursively(new_item, child)
+
+    def on_item_expanded(self, _index: int):
+        self.tree_view.resizeColumnToContents(CommandPathSelectWidget.NODE_NAME_COLUMN)
+        self.tree_view.resizeColumnToContents(
+            CommandPathSelectWidget.DESCRIPTION_COLUMN
+        )
+
+    def select_path_clicked(self):
+        selected_indexes = self.tree_view.selectedIndexes()
+        if selected_indexes:
+            index = selected_indexes[0]  # Get the first selected index
+            item = self.tree_model.itemFromIndex(index)
+            self.currently_selected_path = f"/{self.get_item_path(item)}"
+            self.path_label.setText(f"Selected: {self.currently_selected_path}")
+
+    def copy_path_clicked(self):
+        if self.currently_selected_path is None:
+            self.select_path_clicked()
+        if self.currently_selected_path is not None:
+            print(f"Copied path {self.currently_selected_path} to clipboard.")
+            clipboard = QApplication.clipboard()
+            if clipboard is not None:
+                clipboard.setText(self.currently_selected_path)
+
+    def get_item_path(self, item):
+        path = []
+        while item:
+            path.insert(0, item.text())
+            item = item.parent()
+        return "/".join(path)
diff --git a/tmtccmd/gui/defs.py b/tmtccmd/gui/defs.py
index cff47e62..008453e5 100644
--- a/tmtccmd/gui/defs.py
+++ b/tmtccmd/gui/defs.py
@@ -1,5 +1,7 @@
+from dataclasses import dataclass
 import enum
 import threading
+from typing import Any, Optional
 
 from tmtccmd import CcsdsTmtcBackend
 from tmtccmd.config import CoreComInterfaces
@@ -37,7 +39,7 @@
 )
 
 
-class WorkerOperationsCodes(enum.IntEnum):
+class WorkerOperationsCode(enum.IntEnum):
     OPEN_COM_IF = 0
     CLOSE_COM_IF = 1
     ONE_QUEUE_MODE = 2
@@ -69,7 +71,7 @@ def is_used(self):
 
 
 class LocalArgs:
-    def __init__(self, op_code: WorkerOperationsCodes, op_code_args: any = None):
+    def __init__(self, op_code: WorkerOperationsCode, op_code_args: Any = None):
         self.op_code = op_code
         self.op_args = op_code_args
 
@@ -82,11 +84,12 @@ def __init__(self, backend: CcsdsTmtcBackend):
         self.backend = backend
 
 
+@dataclass
 class FrontendState:
-    def __init__(self):
-        self.current_com_if = CoreComInterfaces.UNSPECIFIED.value
-        self.current_service = ""
-        self.current_op_code = ""
-        self.auto_connect_tm_listener = True
-        self.last_com_if = CoreComInterfaces.UNSPECIFIED.value
-        self.current_com_if_key = CoreComInterfaces.UNSPECIFIED.value
+    current_com_if = CoreComInterfaces.UNSPECIFIED.value
+    current_cmd_path: Optional[str] = None
+    auto_connect_tm_listener = True
+    last_com_if = CoreComInterfaces.UNSPECIFIED.value
+    current_com_if_key = CoreComInterfaces.UNSPECIFIED.value
+    print_tm = False
+    print_raw_tm = False
diff --git a/tmtccmd/gui/frontend.py b/tmtccmd/gui/frontend.py
index 354f01ce..dd418de4 100644
--- a/tmtccmd/gui/frontend.py
+++ b/tmtccmd/gui/frontend.py
@@ -6,9 +6,10 @@
 import webbrowser
 from multiprocessing import Process
 from pathlib import Path
-from typing import Union
+from typing import Any, Tuple
 
 from PyQt6.QtWidgets import (
+    QLineEdit,
     QMainWindow,
     QGridLayout,
     QTableWidget,
@@ -29,7 +30,6 @@
 )
 
 from tmtccmd.core.base import FrontendBase
-from tmtccmd.config.globals import CoreGlobalIds
 from tmtccmd.core.ccsds_backend import CcsdsTmtcBackend
 from tmtccmd.config import HookBase
 from tmtccmd.gui.buttons import (
@@ -39,10 +39,9 @@
     TmButtonWrapper,
     ConnectButtonWrapper,
 )
+from tmtccmd.gui.cmd_select import CommandPathSelectWidget
 from tmtccmd.gui.defs import SharedArgs, CONNECT_BTTN_STYLE, FrontendState
 from tmtccmd.logging import get_console_logger
-from tmtccmd.core.globals_manager import get_global, update_global
-from tmtccmd.com.tcpip_utils import TcpIpConfigIds
 import tmtccmd as mod_root
 
 LOGGER = get_console_logger()
@@ -58,34 +57,22 @@ def __init__(
         self._shared_args = SharedArgs(tmtc_backend)
         tmtc_backend.exit_on_com_if_init_failure = False
         self._hook_obj = hook_obj
-        self._service_list = []
-        self._op_code_list = []
         self._com_if_list = []
-        self._service_op_code_dict = hook_obj.get_tmtc_definitions()
         self._state = FrontendState()
         self._thread_pool = QThreadPool()
-        self.__connected = False
-
-        self.__combo_box_op_codes: Union[None, QComboBox] = None
         self.logo_path = Path(
             f"{Path(mod_root.__file__).parent.parent}/misc/logo-tiny.png"
         )
 
-    def prepare_start(self, args: any) -> Process:
+    def prepare_start(self, _: Any) -> Process:
         return Process(target=self.start)
 
-    def start(self, qt_app: any):
-        self.__start_ui()
+    def start(self, qt_app: Any):
+        self._start_ui()
         sys.exit(qt_app.exec())
 
-    def set_gui_logo(self, logo_total_path: str):
-        if os.path.isfile(logo_total_path):
-            self.logo_path = logo_total_path
-        else:
-            LOGGER.warning("Could not set logo, path invalid!")
-
-    def __start_ui(self):
-        self.__create_menu_bar()
+    def _start_ui(self):
+        self._create_menu_bar()
         win = QWidget(self)
         self.setCentralWidget(win)
 
@@ -93,7 +80,10 @@ def __start_ui(self):
         win.setLayout(grid)
         row = 0
         self.setWindowTitle(self._app_name)
-        self.setWindowIcon(QIcon(self.logo_path.as_posix()))
+        if isinstance(self.logo_path, Path):
+            self.setWindowIcon(QIcon(str(self.logo_path)))
+        else:
+            self.setWindowIcon(QIcon(self.logo_path))
 
         add_pixmap = False
 
@@ -122,7 +112,7 @@ def __start_ui(self):
         tmtc_ctrl_label.setFont(font)
         grid.addWidget(tmtc_ctrl_label, row, 0, 1, 2)
         row += 1
-        row = self.__set_up_service_op_code_section(grid=grid, row=row)
+        row = self._set_up_cmd_path_ui(grid=grid, row=row)
 
         button_args = ButtonArgs(
             state=self._state, pool=self._thread_pool, shared=self._shared_args
@@ -145,6 +135,12 @@ def __start_ui(self):
         self.show()
         # self.destroyed.connect(self.__tm_button_wrapper.stop_thread)
 
+    def set_gui_logo(self, logo_total_path: str):
+        if os.path.isfile(logo_total_path):
+            self.logo_path = logo_total_path
+        else:
+            LOGGER.warning("Could not set logo, path invalid!")
+
     def closeEvent(self, event):
         try:
             pass
@@ -157,13 +153,15 @@ def closeEvent(self, event):
         except KeyboardInterrupt:
             self.__tm_button_wrapper.abort_thread()
 
-    def __create_menu_bar(self):
+    def _create_menu_bar(self):
         menu_bar = self.menuBar()
         # Creating menus using a QMenu object
         file_menu = QMenu("&File", self)
+        assert menu_bar is not None
         menu_bar.addMenu(file_menu)
         # Creating menus using a title
         help_menu = menu_bar.addMenu("&Help")
+        assert help_menu is not None
 
         help_action = QAction("Help", self)
         help_action.triggered.connect(self.__help_url)
@@ -200,7 +198,7 @@ def __set_up_config_section(self, grid: QGridLayout, row: int) -> int:
         spin_timeout.setMaximum(500.0)
         # https://youtrack.jetbrains.com/issue/PY-22908
         # Ignore those warnings for now.
-        spin_timeout.valueChanged.connect(number_timeout)
+        spin_timeout.valueChanged.connect(self.__number_timeout_changed)
         grid.addWidget(spin_timeout, row, 0, 1, 1)
         row += 1
         return row
@@ -213,7 +211,7 @@ def _tm_auto_connect_changed(self, box: QCheckBox):
 
     def __set_up_com_if_section(
         self, conn_bttn_params: ConnectButtonParams, grid: QGridLayout, row: int
-    ) -> (int, ConnectButtonWrapper):
+    ) -> Tuple[int, ConnectButtonWrapper]:
         font = QFont()
         font.setBold(True)
         label = QLabel("Communication Interface")
@@ -265,44 +263,53 @@ def __disconnect_com_if_cb(self):
         self.__send_bttn_wrapper.button.setDisabled(True)
         self.__tm_button_wrapper.button.setDisabled(True)
 
-    def __set_up_service_op_code_section(self, grid: QGridLayout, row: int):
-        grid.addWidget(QLabel("Service: "), row, 0, 1, 2)
-        grid.addWidget(QLabel("Operation Code: "), row, 1, 1, 2)
+    def _set_up_cmd_path_ui(self, grid: QGridLayout, row: int):
+        font = QFont()
+        font.setBold(True)
+        label = QLabel("Communication Select")
+        label.setFont(font)
+        grid.addWidget(label, row, 0, 1, 1)
         row += 1
+        self._open_command_path_select_button = QPushButton("Command Path Select")
+        self._open_command_path_select_button.clicked.connect(
+            self._open_command_select_widget
+        )
+        grid.addWidget(self._open_command_path_select_button, row, 0, 1, 2)
+        row += 1
+        self._cmd_path_text_input = QLineEdit()
+        grid.addWidget(self._cmd_path_text_input, row, 0, 1, 2)
+        row += 1
+        self._confirm_selected_cmd_path_button = QPushButton("Confirm selected path")
+        grid.addWidget(self._confirm_selected_cmd_path_button, row, 0, 1, 2)
+        row += 1
+        self._cmd_path_text = QLabel("Selected command path: ")
+        grid.addWidget(self._cmd_path_text, row, 0, 1, 2)
+        row += 1
+        return row
 
-        combo_box_services = QComboBox()
-        default_service = get_global(CoreGlobalIds.CURRENT_SERVICE)
-        self._service_op_code_dict = self._hook_obj.get_tmtc_definitions()
-        if self._service_op_code_dict is None:
-            LOGGER.warning("Invalid service to operation code dictionary")
-            LOGGER.warning("Setting default dictionary")
-            from tmtccmd.config.globals import get_default_tmtc_defs
+    def _open_command_select_widget(self):
+        self.cmd_select_window = CommandPathSelectWidget(
+            self._hook_obj.get_command_definitions()
+        )
+        self.cmd_select_window.path_selected_sig.connect(self._receive_selected_path)
+        self.cmd_select_window.closed.connect(self._on_treeview_closed)
+        self.cmd_select_window.show()
+        self._open_command_path_select_button.setEnabled(False)
 
-            self._service_op_code_dict = get_default_tmtc_defs()
-        index = 0
-        default_index = 0
-        for service_key, service_value in self._service_op_code_dict.defs.items():
-            combo_box_services.addItem(service_value[0])
-            if service_key == default_service:
-                default_index = index
-            self._service_list.append(service_key)
-            index += 1
-        combo_box_services.setCurrentIndex(default_index)
-        self._state.current_service = self._service_list[default_index]
+    def _set_cmd_path_label(self, text: str):
+        self._cmd_path_text.setText(f"Selected command path: {text}")
 
-        combo_box_services.currentIndexChanged.connect(self.__service_index_changed)
-        grid.addWidget(combo_box_services, row, 0, 1, 1)
+    def _confirm_selected_cmd_path(self):
+        self._set_cmd_path_label(self._cmd_path_text_input.text())
+        self._state.current_cmd_path = self._cmd_path_text_input.text()
 
-        self.__combo_box_op_codes = QComboBox()
-        self._state.current_service = self._service_list[default_index]
-        self.__update_op_code_combo_box()
-        self.__combo_box_op_codes.currentIndexChanged.connect(
-            self.__op_code_index_changed
-        )
-        # TODO: Combo box also needs to be updated if another service is selected
-        grid.addWidget(self.__combo_box_op_codes, row, 1, 1, 1)
-        row += 1
-        return row
+    def _on_treeview_closed(self):
+        if self.cmd_select_window is not None:
+            self._open_command_path_select_button.setEnabled(True)
+
+    def _receive_selected_path(self, path: str):
+        self._cmd_path_text_input.setText(path)
+        self._confirm_selected_cmd_path()
 
     def __set_up_pixmap(self, grid: QGridLayout, row: int) -> int:
         label = QLabel(self)
@@ -315,7 +322,9 @@ def __set_up_pixmap(self, grid: QGridLayout, row: int) -> int:
         row += 1
 
         pixmap_scaled = pixmap.scaled(
-            pixmap_width * 0.3, pixmap_height * 0.3, Qt.AspectRatioMode.KeepAspectRatio
+            int(pixmap_width * 0.3),
+            int(pixmap_height * 0.3),
+            Qt.AspectRatioMode.KeepAspectRatio,
         )
         label.setPixmap(pixmap_scaled)
         label.setScaledContents(True)
@@ -332,58 +341,13 @@ def __add_vertical_separator(grid: QGridLayout, row: int):
         row += 1
         return row
 
-    def __service_index_changed(self, index: int):
-        self._state.current_service = self._service_list[index]
-        self.__update_op_code_combo_box()
-        LOGGER.debug("Service changed")
-
-    def __op_code_index_changed(self, index: int):
-        self._state.current_op_code = self._op_code_list[index]
-        LOGGER.debug("Op Code changed")
-
-    def __update_op_code_combo_box(self):
-        self.__combo_box_op_codes.clear()
-        self._op_code_list = []
-        op_code_entry = self._service_op_code_dict.op_code_entry(
-            self._state.current_service
-        )
-        if op_code_entry is not None:
-            for (
-                op_code_key,
-                op_code_value,
-            ) in op_code_entry.op_code_dict_num_keys.items():
-                try:
-                    self._op_code_list.append(op_code_key)
-                    self.__combo_box_op_codes.addItem(op_code_value[0])
-                except TypeError:
-                    LOGGER.warning(f"Invalid op code entry {op_code_value}, skipping..")
-            for (
-                op_code_key,
-                op_code_value,
-            ) in op_code_entry.op_code_dict_str_keys.items():
-                try:
-                    self._op_code_list.append(op_code_key)
-                    self.__combo_box_op_codes.addItem(op_code_value[0])
-                except TypeError:
-                    LOGGER.warning(f"Invalid op code entry {op_code_value}, skipping..")
-            self._state.current_op_code = self._op_code_list[0]
-
-    def __checkbox_log_update(self, state: int):
-        update_global(CoreGlobalIds.PRINT_TO_FILE, state)
-        LOGGER.debug(["Enabled", "Disabled"][state == 0] + " print to log.")
-
-    def __checkbox_console_update(self, state: bool):
-        update_global(CoreGlobalIds.PRINT_TM, state)
-        LOGGER.debug(["enabled", "disabled"][state == 0] + " console print")
-
-    def __checkbox_print_raw_data_update(self, state: int):
-        update_global(CoreGlobalIds.PRINT_RAW_TM, state)
-        LOGGER.debug(["enabled", "disabled"][state == 0] + " printing of raw data")
-
     def __com_if_sel_index_changed(self, index: int):
         self._state.current_com_if = self._com_if_list[index][0]
         LOGGER.debug(f"Communication IF updated: {self._com_if_list[index][1]}")
 
+    def __number_timeout_changed(self, value: float):
+        LOGGER.info("PUS TM timeout changed to: " + str(value))
+
 
 class SingleCommandTable(QTableWidget):
     def __init__(self):
@@ -398,37 +362,3 @@ def __init__(self):
         self.setItem(0, 0, QTableWidgetItem("17"))
         self.setItem(0, 1, QTableWidgetItem("1"))
         self.setItem(0, 2, QTableWidgetItem("20"))
-
-
-def checkbox_print_hk_data(state: int):
-    update_global(CoreGlobalIds.PRINT_HK, state)
-    LOGGER.info(["enabled", "disabled"][state == 0] + " printing of hk data")
-
-
-def checkbox_short_display_mode(state: int):
-    update_global(CoreGlobalIds.DISPLAY_MODE, state)
-    LOGGER.info(["enabled", "disabled"][state == 0] + " short display mode")
-
-
-def number_timeout(value: float):
-    update_global(CoreGlobalIds.TM_TIMEOUT, value)
-    LOGGER.info("PUS TM timeout changed to: " + str(value))
-
-
-def number_timeout_factor(value: float):
-    update_global(CoreGlobalIds.TC_SEND_TIMEOUT_FACTOR, value)
-    LOGGER.info("PUS TM timeout factor changed to: " + str(value))
-
-
-def ip_change_client(value):
-    ethernet_config = get_global(CoreGlobalIds.ETHERNET_CONFIG)
-    ethernet_config[TcpIpConfigIds.RECV_ADDRESS] = value
-    update_global(CoreGlobalIds.ETHERNET_CONFIG, ethernet_config)
-    LOGGER.info("Client IP changed: " + value)
-
-
-def ip_change_board(value):
-    ethernet_config = get_global(CoreGlobalIds.ETHERNET_CONFIG)
-    ethernet_config[TcpIpConfigIds.SEND_ADDRESS] = value
-    update_global(CoreGlobalIds.ETHERNET_CONFIG, ethernet_config)
-    LOGGER.info("Board IP changed: " + value)
diff --git a/tmtccmd/gui/worker.py b/tmtccmd/gui/worker.py
index b743f160..35ede554 100644
--- a/tmtccmd/gui/worker.py
+++ b/tmtccmd/gui/worker.py
@@ -1,11 +1,14 @@
+from __future__ import annotations
 import logging
 import time
-from typing import Optional
+from typing import Optional, Any
 
 from PyQt6.QtCore import QRunnable, pyqtSlot, QObject, pyqtSignal
+from tmtccmd.config.hook import HookBase
 
 from tmtccmd.core import TmMode, TcMode, BackendRequest
-from tmtccmd.gui.defs import LocalArgs, SharedArgs, WorkerOperationsCodes
+from tmtccmd.gui.defs import LocalArgs, SharedArgs, WorkerOperationsCode
+from tmtccmd.tmtc.procedure import DefaultProcedureInfo
 
 
 LOGGER = logging.getLogger(__name__)
@@ -19,10 +22,6 @@ class WorkerSignalWrapper(QObject):
 
 
 class FrontendWorker(QRunnable):
-    """Runnable thread which can be used with QThreadPool. Not used for now, might be needed
-    in the future.
-    """
-
     def __init__(self, local_args: LocalArgs, shared_args: SharedArgs):
         super(QRunnable, self).__init__()
         self._locals = local_args
@@ -33,22 +32,45 @@ def __init__(self, local_args: LocalArgs, shared_args: SharedArgs):
         self.signals.stop.connect(self._stop_com_if)
         self.signals.abort.connect(self._abort)
 
-    def __sanitize_locals(self):
-        if self._locals.op_code == WorkerOperationsCodes.LISTEN_FOR_TM:
-            if self._locals.op_args is None or not isinstance(
-                float, self._locals.op_args
-            ):
-                self._locals.op_args = 0.2
-
-    def __setup(self, op_code: WorkerOperationsCodes) -> bool:
-        if op_code == WorkerOperationsCodes.OPEN_COM_IF:
+    @classmethod
+    def spawn_for_opening_com_if(
+        cls,
+        com_if_is_switched: bool,
+        com_if_key: str,
+        hook: HookBase,
+        shared_args: SharedArgs,
+    ):
+        return cls(
+            LocalArgs(
+                WorkerOperationsCode.OPEN_COM_IF, (com_if_is_switched, com_if_key, hook)
+            ),
+            shared_args,
+        )
+
+    @classmethod
+    def spawn_for_cmd_path(
+        cls, cmd_path: str, shared_args: SharedArgs
+    ) -> FrontendWorker:
+        return cls(
+            LocalArgs(WorkerOperationsCode.ONE_QUEUE_MODE, cmd_path), shared_args
+        )
+
+    def __setup(self, op_code: WorkerOperationsCode) -> bool:
+        if op_code == WorkerOperationsCode.OPEN_COM_IF:
             LOGGER.info("Switching COM Interface")
+
+            assert isinstance(self._locals.op_args, tuple)
+            assert isinstance(self._locals.op_args[0], bool)
+            assert isinstance(self._locals.op_args[1], str)
+            assert isinstance(self._locals.op_args[2], HookBase)
             # TODO: We should really pass a proper object here instead of using magic tuples..
-            new_com_if = self._locals.op_args[2].assign_communication_interface(
+            new_com_if = self._locals.op_args[2].get_communication_interface(
                 com_if_key=self._locals.op_args[1]
             )
             # self._args.state.last_com_if = self._args.state.current_com_if
-            set_success = self._shared.backend.try_set_com_if(new_com_if)
+            set_success = False
+            if new_com_if is not None and self._locals.op_args[0]:
+                set_success = self._shared.backend.try_set_com_if(new_com_if)
             if not set_success:
                 LOGGER.warning(
                     f"Could not set new communication interface {new_com_if}"
@@ -59,7 +81,7 @@ def __setup(self, op_code: WorkerOperationsCodes) -> bool:
                 self._shared.backend.open_com_if()
                 self._finish_success()
             return False
-        if op_code == WorkerOperationsCodes.CLOSE_COM_IF:
+        if op_code == WorkerOperationsCode.CLOSE_COM_IF:
             if not self._shared.backend.com_if_active():
                 self._finish_with_info("COM Interface is not active")
             elif self._shared.com_if_ref_tracker.is_used():
@@ -68,11 +90,15 @@ def __setup(self, op_code: WorkerOperationsCodes) -> bool:
                 self._shared.backend.close_com_if()
                 self._finish_success()
             return False
-        if op_code == WorkerOperationsCodes.ONE_QUEUE_MODE:
+        if op_code == WorkerOperationsCode.ONE_QUEUE_MODE:
             self._shared.com_if_ref_tracker.add_user()
+            assert isinstance(self._locals.op_args, str)
             with self._shared.tc_lock:
+                self._shared.backend.current_procedure = DefaultProcedureInfo(
+                    self._locals.op_args
+                )
                 self._shared.backend.tc_mode = TcMode.ONE_QUEUE
-        elif op_code == WorkerOperationsCodes.LISTEN_FOR_TM:
+        elif op_code == WorkerOperationsCode.LISTEN_FOR_TM:
             self._shared.com_if_ref_tracker.add_user()
             self._shared.backend.tm_mode = TmMode.LISTENER
         return True
@@ -85,12 +111,6 @@ def __one_queue_mode_cycle(self) -> Optional[bool]:
         if state.request == BackendRequest.TERMINATION_NO_ERROR:
             self._shared.tc_lock.release()
             self._shared.com_if_ref_tracker.remove_user()
-            with self._shared.state_lock:
-                if (
-                    not self._shared.com_if_ref_tracker.is_used()
-                    and self._locals.op_args is not None
-                ):
-                    self._locals.op_args()
             self._finish_success()
             return False
         elif state.request == BackendRequest.DELAY_IDLE:
@@ -116,16 +136,16 @@ def __listener_cycle(self) -> Optional[bool]:
             # Poll TM every 400 ms for now
             time.sleep(self._locals.op_args)
 
-    def __loop(self, op_code: WorkerOperationsCodes) -> bool:
-        if op_code == WorkerOperationsCodes.ONE_QUEUE_MODE:
+    def __loop(self, op_code: WorkerOperationsCode) -> bool:
+        if op_code == WorkerOperationsCode.ONE_QUEUE_MODE:
             should_return = self.__one_queue_mode_cycle()
             if should_return is not None:
                 return should_return
-        elif op_code == WorkerOperationsCodes.LISTEN_FOR_TM:
+        elif op_code == WorkerOperationsCode.LISTEN_FOR_TM:
             should_return = self.__listener_cycle()
             if should_return is not None:
                 return should_return
-        elif op_code == WorkerOperationsCodes.IDLE:
+        elif op_code == WorkerOperationsCode.IDLE:
             return False
         else:
             # This must be a programming error
@@ -154,8 +174,8 @@ def _update_backend_mode(self):
         with self._shared.state_lock:
             self._shared.backend.mode_to_req()
 
-    def _stop_com_if(self, _args: any):
+    def _stop_com_if(self, _args: Any):
         self._stop_signal = True
 
-    def _abort(self, _args: any):
+    def _abort(self, _args: Any):
         self._abort_signal = True
diff --git a/tmtccmd/logging/__init__.py b/tmtccmd/logging/__init__.py
index f38df06f..7bbdd4ef 100644
--- a/tmtccmd/logging/__init__.py
+++ b/tmtccmd/logging/__init__.py
@@ -7,7 +7,7 @@
 import sys
 from datetime import datetime
 
-import deprecation
+from deprecated.sphinx import deprecated
 from colorlog import ColoredFormatter
 
 
@@ -61,10 +61,9 @@ def add_error_file_logger(logger: logging.Logger):
     logger.addHandler(error_file_handler)
 
 
-@deprecation.deprecated(
-    deprecated_in="v4.0.0a4",
-    current_version=get_version(),
-    details="Use a custom application logger or tmtccmd.get_lib_logger instead.",
+@deprecated(
+    version="v4.0.0a4",
+    reason="use a custom application logger or tmtccmd.get_lib_logger instead.",
 )
 def get_console_logger() -> logging.Logger:
     return logging.getLogger(__name__)
diff --git a/tmtccmd/pus/tc/s11_tc_sched.py b/tmtccmd/pus/tc/s11_tc_sched.py
index eb94c34a..199a4c23 100644
--- a/tmtccmd/pus/tc/s11_tc_sched.py
+++ b/tmtccmd/pus/tc/s11_tc_sched.py
@@ -1,6 +1,4 @@
-import deprecation
-
-from tmtccmd.version import get_version
+from deprecated.sphinx import deprecated
 from spacepackets.ecss import PusTelecommand, PusService
 from tmtccmd.pus.s11_tc_sched import Subservice
 
@@ -16,10 +14,9 @@ def __generic_param_less_tc_sched_cmd(
     )
 
 
-@deprecation.deprecated(
-    deprecated_in="4.0.0a2",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="4.0.0a2",
+    reason="use create... API instead",
 )
 def generate_enable_tc_sched_cmd(apid: int = 0, seq_count: int = 0) -> PusTelecommand:
     return create_enable_tc_sched_cmd(apid, seq_count)
@@ -31,10 +28,9 @@ def create_enable_tc_sched_cmd(apid: int = 0, seq_count: int = 0) -> PusTelecomm
     )
 
 
-@deprecation.deprecated(
-    deprecated_in="4.0.0a2",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="4.0.0a2",
+    reason="use create... API instead",
 )
 def generate_disable_tc_sched_cmd(apid: int = 0, seq_count: int = 0) -> PusTelecommand:
     return create_disable_tc_sched_cmd(apid, seq_count)
@@ -46,10 +42,9 @@ def create_disable_tc_sched_cmd(apid: int = 0, seq_count: int = 0) -> PusTelecom
     )
 
 
-@deprecation.deprecated(
-    deprecated_in="4.0.0a2",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="4.0.0a2",
+    reason="use create... API instead",
 )
 def generate_reset_tc_sched_cmd(apid: int = 0, seq_count: int = 0) -> PusTelecommand:
     return create_reset_tc_sched_cmd(apid, seq_count)
@@ -61,10 +56,9 @@ def create_reset_tc_sched_cmd(apid: int = 0, seq_count: int = 0) -> PusTelecomma
     )
 
 
-@deprecation.deprecated(
-    deprecated_in="4.0.0a2",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="4.0.0a2",
+    reason="use create... API instead",
 )
 def generate_time_tagged_cmd(release_time: bytes, tc_to_insert: PusTelecommand):
     return create_time_tagged_cmd(release_time, tc_to_insert)
diff --git a/tmtccmd/pus/tc/s200_fsfw_mode.py b/tmtccmd/pus/tc/s200_fsfw_mode.py
index ce38e0f7..dd160a4a 100644
--- a/tmtccmd/pus/tc/s200_fsfw_mode.py
+++ b/tmtccmd/pus/tc/s200_fsfw_mode.py
@@ -1,11 +1,10 @@
 """Core components for mode commanding (custom PUS service)."""
 import enum
 import struct
-import deprecation
 from typing import Union
 
+from deprecated.sphinx import deprecated
 from spacepackets.ecss import PusTelecommand
-from tmtccmd.version import get_version
 from tmtccmd.pus import CustomFsfwPusService
 from tmtccmd.pus.s200_fsfw_mode import Subservice
 
@@ -27,10 +26,9 @@ def pack_mode_data(object_id: bytes, mode: Union[Mode, int], submode: int) -> by
     return mode_data
 
 
-@deprecation.deprecated(
-    deprecated_in="v4.0.0a2",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="v4.0.0a2",
+    reason="use create... API instead",
 )
 def pack_mode_command(
     object_id: bytes, mode: Union[int, Mode], submode: int
diff --git a/tmtccmd/pus/tc/s20_fsfw_param.py b/tmtccmd/pus/tc/s20_fsfw_param.py
index ee5622d4..54193c38 100644
--- a/tmtccmd/pus/tc/s20_fsfw_param.py
+++ b/tmtccmd/pus/tc/s20_fsfw_param.py
@@ -1,33 +1,33 @@
 """Contains definitions and functions related to PUS Service 20 Telecommands.
 """
 from __future__ import annotations
+
 import struct
+from typing import Optional
 
+from deprecated.sphinx import deprecated
 from spacepackets.ecss import PusService
-from tmtccmd.version import get_version
-from deprecation import deprecated
-from typing import Optional
+from spacepackets.ecss.fields import PfcReal, PfcUnsigned, Ptc
+from spacepackets.ecss.tc import PusTelecommand
 
-from spacepackets.ecss.fields import Ptc, PfcUnsigned, PfcReal
 from tmtccmd.pus.s20_fsfw_param_defs import (  # noqa: F401
     CustomSubservice,
+    FsfwParamId,
+    Parameter,
+    create_matrix_double_parameter,
+    create_matrix_float_parameter,
     create_scalar_boolean_parameter,
-    create_scalar_u8_parameter,
-    create_scalar_u16_parameter,
-    create_scalar_u32_parameter,
+    create_scalar_double_parameter,
+    create_scalar_float_parameter,
     create_scalar_i8_parameter,
     create_scalar_i16_parameter,
     create_scalar_i32_parameter,
-    create_scalar_float_parameter,
-    create_scalar_double_parameter,
+    create_scalar_u8_parameter,
+    create_scalar_u16_parameter,
+    create_scalar_u32_parameter,
     create_vector_double_parameter,
     create_vector_float_parameter,
-    create_matrix_float_parameter,
-    create_matrix_double_parameter,
-    Parameter,
-    FsfwParamId,
 )
-from spacepackets.ecss.tc import PusTelecommand
 
 
 def create_load_param_cmd(parameter: Parameter) -> PusTelecommand:
@@ -55,18 +55,16 @@ def create_load_param_cmd_from_raw(parameter_raw: bytes) -> PusTelecommand:
 
 
 @deprecated(
-    deprecated_in="4.0.0a3",
-    current_version=get_version(),
-    details="Please use crate_fsfw_load_param_cmd instead",
+    version="4.0.0a3",
+    reason="use crate_fsfw_load_param_cmd instead",
 )
 def pack_fsfw_load_param_cmd(app_data: bytes) -> PusTelecommand:
     return create_load_param_cmd(app_data)
 
 
 @deprecated(
-    deprecated_in="3.1.0",
-    current_version=get_version(),
-    details="Please use create_scalar_boolean_parameter instead",
+    version="3.1.0",
+    reason="use create_scalar_boolean_parameter instead",
 )
 def pack_boolean_parameter_app_data(
     object_id: bytes, domain_id: int, unique_id: int, parameter: bool
@@ -77,9 +75,8 @@ def pack_boolean_parameter_app_data(
 
 
 @deprecated(
-    deprecated_in="4.0.0a2",
-    current_version=get_version(),
-    details="Please use create_scalar_boolean_parameter instead",
+    version="4.0.0a2",
+    reason="Please use create_scalar_boolean_parameter instead",
 )
 def pack_scalar_boolean_parameter_app_data(
     object_id: bytes, domain_id: int, unique_id: int, parameter: bool
@@ -98,9 +95,8 @@ def pack_scalar_boolean_parameter_app_data(
 
 
 @deprecated(
-    deprecated_in="4.0.0a2",
-    current_version=get_version(),
-    details="Please use create_scalar_u8_parameter instead",
+    version="4.0.0a2",
+    reason="Please use create_scalar_u8_parameter instead",
 )
 def pack_scalar_u8_parameter_app_data(
     object_id: bytes, domain_id: int, unique_id: int, parameter: int
@@ -128,9 +124,8 @@ def pack_scalar_u8_parameter_app_data(
 
 
 @deprecated(
-    deprecated_in="4.0.0a2",
-    current_version=get_version(),
-    details="Please use create_scalar_double_parameter instead",
+    version="4.0.0a2",
+    reason="Please use create_scalar_double_parameter instead",
 )
 def pack_scalar_double_param_app_data(
     object_id: bytes, domain_id: int, unique_id: int, parameter: float
@@ -150,9 +145,8 @@ def pack_scalar_double_param_app_data(
 
 
 @deprecated(
-    deprecated_in="4.0.0a2",
-    current_version=get_version(),
-    details="Please use create_scalar_float_parameter instead",
+    version="4.0.0a2",
+    reason="use create_scalar_float_parameter instead",
 )
 def pack_scalar_float_param_app_data(
     object_id: bytes, domain_id: int, unique_id: int, parameter: float
@@ -172,9 +166,8 @@ def pack_scalar_float_param_app_data(
 
 
 @deprecated(
-    deprecated_in="4.0.0a2",
-    current_version=get_version(),
-    details="Please use ParamWrapper helper class with pack() instead",
+    version="4.0.0a2",
+    reason="use ParamWrapper helper class with pack() instead",
 )
 def prepare_param_packet_header(
     object_id: bytes,
@@ -202,9 +195,8 @@ def prepare_param_packet_header(
 
 
 @deprecated(
-    deprecated_in="4.0.0a2",
-    current_version=get_version(),
-    details="Please use ParamWrapper helper class with pack() instead",
+    version="4.0.0a2",
+    reason="use ParamWrapper helper class with pack() instead",
 )
 def pack_type_and_matrix_data(ptc: int, pfc: int, rows: int, columns: int) -> bytearray:
     data = bytearray(4)
@@ -216,9 +208,8 @@ def pack_type_and_matrix_data(ptc: int, pfc: int, rows: int, columns: int) -> by
 
 
 @deprecated(
-    deprecated_in="4.0.0a2",
-    current_version=get_version(),
-    details="Please use ParameterId helper class with pack() instead",
+    version="4.0.0a2",
+    reason="use ParameterId helper class with pack() instead",
 )
 def pack_parameter_id(domain_id: int, unique_id: int, linear_index: int) -> bytearray:
     """Packs the Parameter ID (bytearray with 4 bytes) which is part of the service 20 packets.
diff --git a/tmtccmd/pus/tc/s3_fsfw_hk.py b/tmtccmd/pus/tc/s3_fsfw_hk.py
index 79a13c1a..69d9ffd2 100644
--- a/tmtccmd/pus/tc/s3_fsfw_hk.py
+++ b/tmtccmd/pus/tc/s3_fsfw_hk.py
@@ -1,10 +1,9 @@
 """Contains definitions and functions related to PUS Service 3 Telecommands.
 """
 import struct
-import deprecation
 from typing import Tuple
 
-from tmtccmd.version import get_version
+from deprecated.sphinx import deprecated
 from spacepackets.ecss.tc import PusTelecommand
 from spacepackets.ecss.pus_3_hk import Subservice
 
@@ -19,10 +18,9 @@ def make_interval(interval_seconds: float) -> bytearray:
     return bytearray(struct.pack("!f", interval_seconds))
 
 
-@deprecation.deprecated(
-    deprecated_in="v4.0.0a2",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="v4.0.0a2",
+    reason="use create... API instead",
 )
 def enable_periodic_hk_command(diag: bool, sid: bytes) -> PusTelecommand:
     return create_enable_periodic_hk_command_with_diag(diag, sid)
@@ -32,10 +30,9 @@ def create_enable_periodic_hk_command(sid: bytes) -> PusTelecommand:
     return __generate_periodic_hk_command(enable=True, sid=sid)
 
 
-@deprecation.deprecated(
-    deprecated_in="v6.0.0rc0",
-    current_version=get_version(),
-    details="use diagnostic agnostic API if possible",
+@deprecated(
+    version="v6.0.0rc0",
+    reason="use diagnostic agnostic API if possible",
 )
 def create_enable_periodic_hk_command_with_diag(
     diag: bool, sid: bytes
@@ -43,10 +40,9 @@ def create_enable_periodic_hk_command_with_diag(
     return __generate_periodic_hk_command_legacy(diag=diag, enable=True, sid=sid)
 
 
-@deprecation.deprecated(
-    deprecated_in="v6.0.0rc0",
-    current_version=get_version(),
-    details="use diagnostic agnostic API if possible",
+@deprecated(
+    version="v6.0.0rc0",
+    reason="use diagnostic agnostic API if possible",
 )
 def create_enable_periodic_hk_command_with_interval_with_diag(
     diag: bool, sid: bytes, interval_seconds: float
@@ -64,10 +60,9 @@ def create_enable_periodic_hk_command_with_interval(
     return cmd0, cmd1
 
 
-@deprecation.deprecated(
-    deprecated_in="v4.0.0a2",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="v4.0.0a2",
+    reason="use create... API instead",
 )
 def enable_periodic_hk_command_with_interval(
     diag: bool, sid: bytes, interval_seconds: float
@@ -81,10 +76,9 @@ def create_disable_periodic_hk_command(sid: bytes) -> PusTelecommand:
     return __generate_periodic_hk_command(enable=False, sid=sid)
 
 
-@deprecation.deprecated(
-    deprecated_in="v6.0.0rc0",
-    current_version=get_version(),
-    details="use diagnostic agnostic API if possible",
+@deprecated(
+    version="v6.0.0rc0",
+    reason="use diagnostic agnostic API if possible",
 )
 def create_disable_periodic_hk_command_with_diag(
     diag: bool, sid: bytes
@@ -92,10 +86,9 @@ def create_disable_periodic_hk_command_with_diag(
     return __generate_periodic_hk_command_legacy(diag=diag, enable=False, sid=sid)
 
 
-@deprecation.deprecated(
-    deprecated_in="v4.0.0a2",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="v4.0.0a2",
+    reason="use create... API instead",
 )
 def disable_periodic_hk_command(diag: bool, sid: bytes) -> PusTelecommand:
     return create_disable_periodic_hk_command_with_diag(diag, sid)
@@ -127,10 +120,9 @@ def __generate_periodic_hk_command_legacy(
     return PusTelecommand(service=3, subservice=subservice, app_data=app_data)
 
 
-@deprecation.deprecated(
-    deprecated_in="v4.0.0a2",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="v4.0.0a2",
+    reason="use create... API instead",
 )
 def modify_collection_interval(
     diag: bool, sid: bytes, interval_seconds: float
@@ -147,10 +139,9 @@ def create_modify_collection_interval_cmd(
     return PusTelecommand(service=3, subservice=subservice, app_data=app_data)
 
 
-@deprecation.deprecated(
-    deprecated_in="v6.0.0rc0",
-    current_version=get_version(),
-    details="use diagnostic agnostic API if possible",
+@deprecated(
+    version="v6.0.0rc0",
+    reason="use diagnostic agnostic API if possible",
 )
 def create_modify_collection_interval_cmd_with_diag(
     diag: bool, sid: bytes, interval_seconds: float
@@ -164,10 +155,9 @@ def create_modify_collection_interval_cmd_with_diag(
     return PusTelecommand(service=3, subservice=subservice, app_data=app_data)
 
 
-@deprecation.deprecated(
-    deprecated_in="v4.0.0a2",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="v4.0.0a2",
+    reason="use create... API instead",
 )
 def generate_one_hk_command(sid: bytes) -> PusTelecommand:
     return create_request_one_hk_command(sid)
@@ -181,10 +171,9 @@ def create_request_one_hk_command(sid: bytes) -> PusTelecommand:
     )
 
 
-@deprecation.deprecated(
-    deprecated_in="v4.0.0a2",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="v4.0.0a2",
+    reason="use create... API instead",
 )
 def generate_one_diag_command(sid: bytes) -> PusTelecommand:
     return create_request_one_diag_command(sid)
diff --git a/tmtccmd/pus/tc/s5_event.py b/tmtccmd/pus/tc/s5_event.py
index 8d6370b2..ea2d627e 100644
--- a/tmtccmd/pus/tc/s5_event.py
+++ b/tmtccmd/pus/tc/s5_event.py
@@ -1,16 +1,14 @@
 """Contains definitions and functions related to PUS Service 5 Telecommands.
 """
-import deprecation
 
-from tmtccmd.version import get_version
+from deprecated.sphinx import deprecated
 from spacepackets.ecss import PusTelecommand
 from spacepackets.ecss.pus_5_event import Subservice
 
 
-@deprecation.deprecated(
-    deprecated_in="v4.0.0a0",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="v4.0.0a0",
+    reason="use create... API instead",
 )
 def pack_enable_event_reporting_command(
     apid: int = 0, seq_count: int = 0
@@ -29,10 +27,9 @@ def create_enable_event_reporting_command(
     )
 
 
-@deprecation.deprecated(
-    deprecated_in="v4.0.0a0",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="v4.0.0a0",
+    reason="use create... API instead",
 )
 def pack_disable_event_reporting_command(
     apid: int = 0, seq_count: int = 0
diff --git a/tmtccmd/pus/tc/s8_fsfw_action.py b/tmtccmd/pus/tc/s8_fsfw_action.py
index b06a3f69..20da0f9f 100644
--- a/tmtccmd/pus/tc/s8_fsfw_action.py
+++ b/tmtccmd/pus/tc/s8_fsfw_action.py
@@ -1,16 +1,14 @@
 import struct
 
-import deprecation
-
+from deprecated.sphinx import deprecated
 from spacepackets.ecss import PusTelecommand
+
 from tmtccmd.pus.s8_fsfw_action_defs import CustomSubservice
-from tmtccmd.version import get_version
 
 
-@deprecation.deprecated(
-    deprecated_in="v4.0.0a2",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="v4.0.0a2",
+    reason="use create... API instead",
 )
 def make_fsfw_action_cmd(
     object_id: bytes,
diff --git a/tmtccmd/pus/tm/s200_fsfw_mode.py b/tmtccmd/pus/tm/s200_fsfw_mode.py
index 1e989475..c67e6a15 100644
--- a/tmtccmd/pus/tm/s200_fsfw_mode.py
+++ b/tmtccmd/pus/tm/s200_fsfw_mode.py
@@ -1,16 +1,17 @@
 """Base class for Service 200 mode commanding reply handling.
 """
 from __future__ import annotations
+
 import struct
 from typing import Optional
-from deprecation import deprecated
 
+from deprecated.sphinx import deprecated
 from spacepackets.ccsds.time import CcsdsTimeProvider
 from spacepackets.ecss.tm import CdsShortTimestamp, PusTelemetry
 
 from tmtccmd.pus import CustomFsfwPusService
 from tmtccmd.pus.s200_fsfw_mode import Subservice
-from tmtccmd.tmtc.base import PusTmInfoBase, PusTmBase
+from tmtccmd.tmtc.base import PusTmBase, PusTmInfoBase
 
 
 class Service200FsfwReader:
@@ -41,7 +42,7 @@ def is_cant_reach_mode_reply(self) -> bool:
 
 
 class Service200FsfwTm(PusTmBase, PusTmInfoBase):
-    @deprecated(deprecated_in="7.0.0", details="use Service200FsfwReader instead")
+    @deprecated(version="7.0.0", reason="use Service200FsfwReader instead")
     def __init__(
         self,
         subservice_id: int,
diff --git a/tmtccmd/pus/tm/s2_rawcmd.py b/tmtccmd/pus/tm/s2_rawcmd.py
index fbeebe08..453a2b5b 100644
--- a/tmtccmd/pus/tm/s2_rawcmd.py
+++ b/tmtccmd/pus/tm/s2_rawcmd.py
@@ -4,20 +4,17 @@
 
 from typing import Optional
 
-import deprecation
-
+from deprecated.sphinx import deprecated
 from spacepackets.ccsds.time import CcsdsTimeProvider
 from spacepackets.ecss.tm import CdsShortTimestamp, PusTelemetry
 
-from tmtccmd.version import get_version
-from tmtccmd.tmtc.base import PusTmInfoBase, PusTmBase
+from tmtccmd.tmtc.base import PusTmBase, PusTmInfoBase
 
 
 class Service2Tm(PusTmInfoBase, PusTmBase):
-    @deprecation.deprecated(
-        deprecated_in="v4.0.0a1",
-        current_version=get_version(),
-        details="use a custom wrapper type instead",
+    @deprecated(
+        version="v4.0.0a1",
+        reason="use a custom wrapper type instead",
     )
     def __init__(
         self,
diff --git a/tmtccmd/tmtc/ccsds_seq_sender.py b/tmtccmd/tmtc/ccsds_seq_sender.py
index e69cc828..91a91de0 100644
--- a/tmtccmd/tmtc/ccsds_seq_sender.py
+++ b/tmtccmd/tmtc/ccsds_seq_sender.py
@@ -68,13 +68,13 @@ def queue_wrapper(self, queue_wrapper: QueueWrapper):
         # only
         self._send_cd.timeout = timedelta()
         self._current_res.longest_rem_delay = queue_wrapper.inter_cmd_delay
-        self._proc_wrapper.base = self._queue_wrapper.info
+        self._proc_wrapper.procedure = self._queue_wrapper.info
         self._queue_wrapper = queue_wrapper
 
     def handle_new_queue_forced(self, queue_wrapper: QueueWrapper):
         self._mode = SenderMode.DONE
         self.queue_wrapper = queue_wrapper
-        self._proc_wrapper.base = self._queue_wrapper.info
+        self._proc_wrapper.procedure = self._queue_wrapper.info
 
     def resume(self):
         """Can be used to resume a finished sequential sender it the provided queue is
@@ -104,7 +104,7 @@ def _handle_current_tc_queue(self, com_if: ComInterface):
         if not self.queue_wrapper.queue:
             self._current_res.queue_empty = True
             if self.no_delay_remaining():
-                self._proc_wrapper.base = self._queue_wrapper.info
+                self._proc_wrapper.procedure = self._queue_wrapper.info
                 # cache this for last wait time
                 self._tc_handler.queue_finished_cb(self._proc_wrapper)
                 self._mode = SenderMode.DONE
diff --git a/tmtccmd/tmtc/procedure.py b/tmtccmd/tmtc/procedure.py
index f23aefbe..c933ee92 100644
--- a/tmtccmd/tmtc/procedure.py
+++ b/tmtccmd/tmtc/procedure.py
@@ -17,12 +17,12 @@ def __init__(self, ptype: TcProcedureType):
 
 
 class CustomProcedureInfo(TcProcedureBase):
-    def __init__(self, info: Any):
+    def __init__(self, procedure: Any):
         super().__init__(TcProcedureType.CUSTOM)
-        self.info = info
+        self.procedure = procedure
 
     def __repr__(self):
-        return f"{self.__class__.__name__}(info={self.info!r}"
+        return f"{self.__class__.__name__}(info={self.procedure!r}"
 
 
 class DefaultProcedureInfo(TcProcedureBase):
@@ -30,20 +30,19 @@ class DefaultProcedureInfo(TcProcedureBase):
     of commands. Generally, one procedure is mapped to a specific TC queue which is packed
     during run-time"""
 
-    def __init__(self, service: str, op_code: str):
+    def __init__(self, cmd_path: Optional[str]):
         super().__init__(TcProcedureType.DEFAULT)
-        self.service = service
-        self.op_code = op_code
+        self.cmd_path = cmd_path
 
     @classmethod
     def empty(cls):
-        return cls("", "")
+        return cls(None)
 
     def __repr__(self):
-        return f"CmdInfo(service={self.service!r}, op_code={self.op_code!r})"
+        return f"CmdInfo(cmd_path={self.cmd_path!r})"
 
     def __eq__(self, other: DefaultProcedureInfo) -> bool:
-        return self.service == other.service and self.op_code == other.op_code
+        return self.cmd_path == other.cmd_path
 
 
 class CfdpProcedureInfo(TcProcedureBase):
@@ -60,15 +59,16 @@ class ProcedureWrapper:
     """Procedure helper class. It wraps the concrete procedure object but allows easily casting
     it to concrete types supported by the framework."""
 
-    def __init__(self, base: Optional[TcProcedureBase]):
-        self.base = base
+    def __init__(self, procedure: Optional[TcProcedureBase]):
+        self.procedure = procedure
 
     def __repr__(self):
-        return f"{self.__class__.__name__}(base={self.base!r})"
+        return f"{self.__class__.__name__}(base={self.procedure!r})"
 
     @property
     def proc_type(self):
-        return self.base.ptype
+        assert self.procedure is not None
+        return self.procedure.ptype
 
     def __cast_internally(
         self,
@@ -76,21 +76,25 @@ def __cast_internally(
         obj: TcProcedureBase,
         expected_type: TcProcedureType,
     ) -> Any:
+        assert self.procedure is not None
         if obj.ptype != expected_type:
-            raise TypeError(f"Invalid object {obj} for type {self.base.ptype}")
+            raise TypeError(f"Invalid object {obj} for type {self.procedure.ptype}")
         return cast(obj_type, obj)
 
     def to_def_procedure(self) -> DefaultProcedureInfo:
+        assert self.procedure is not None
         return self.__cast_internally(
-            DefaultProcedureInfo, self.base, TcProcedureType.DEFAULT
+            DefaultProcedureInfo, self.procedure, TcProcedureType.DEFAULT
         )
 
     def to_cfdp_procedure(self) -> CfdpProcedureInfo:
+        assert self.procedure is not None
         return self.__cast_internally(
-            CfdpProcedureInfo, self.base, TcProcedureType.CFDP
+            CfdpProcedureInfo, self.procedure, TcProcedureType.CFDP
         )
 
     def to_custom_procedure(self) -> CustomProcedureInfo:
+        assert self.procedure is not None
         return self.__cast_internally(
-            CustomProcedureInfo, self.base, TcProcedureType.CUSTOM
+            CustomProcedureInfo, self.procedure, TcProcedureType.CUSTOM
         )
diff --git a/tmtccmd/util/countdown.py b/tmtccmd/util/countdown.py
index acdadc76..6a48df0f 100644
--- a/tmtccmd/util/countdown.py
+++ b/tmtccmd/util/countdown.py
@@ -2,9 +2,8 @@
 
 import time
 from typing import Optional
-from deprecation import deprecated
+from deprecated.sphinx import deprecated
 from datetime import timedelta
-from tmtccmd.version import get_version
 
 
 def time_ms() -> int:
@@ -63,9 +62,8 @@ def time_out(self):
         self._start_time_ms = 0
 
     @deprecated(
-        deprecated_in="7.0.0",
-        details="use remaining_time method instead",
-        current_version=get_version(),
+        version="7.0.0",
+        reason="use remaining_time method instead",
     )
     def rem_time(self):
         return self.remaining_time()


Test patch:
diff --git a/tests/config/test_args_conversion.py b/tests/config/test_args_conversion.py
index 0c467907..ee02784e 100644
--- a/tests/config/test_args_conversion.py
+++ b/tests/config/test_args_conversion.py
@@ -1,17 +1,22 @@
 import argparse
+import os
 from unittest import TestCase
-
-from tests.hook_obj_mock import create_hook_mock
+from unittest.mock import MagicMock, call, patch
 
 from spacepackets.cfdp import TransmissionMode
-from tmtccmd import CoreModeList, CoreModeConverter
+
+from tests.hook_obj_mock import create_hook_mock
+from tmtccmd import CoreModeConverter, CoreModeList
 from tmtccmd.config import CfdpParams
 from tmtccmd.config.args import (
+    AppParams,
+    DefaultProcedureParams,
+    SetupParams,
     args_to_all_params_tmtc,
     cfdp_args_to_cfdp_params,
-    SetupParams,
-    DefaultProcedureParams,
+    perform_tree_printout,
 )
+from tmtccmd.config.tmtc import CmdTreeNode, TreePart
 
 
 class TestArgs(TestCase):
@@ -27,14 +32,13 @@ def base_cli_set(self):
         self.pargs.com_if = "dummy"
         self.pargs.listener = True
         self.pargs.prompt_proc = False
-        self.pargs.service = None
-        self.pargs.op_code = None
+        self.pargs.cmd_path = None
+        self.pargs.print_tree = None
 
     def simple_pargs_cli_set(self):
         self.base_cli_set()
         self.pargs.listener = False
-        self.pargs.service = "17"
-        self.pargs.op_code = "ping"
+        self.pargs.cmd_path = "/PING"
 
     def auto_listener_cli_set(self):
         self.base_cli_set()
@@ -48,7 +52,7 @@ def test_basic(self):
         self.assertEqual(self.params.tc_params.delay, 0)
         self.assertEqual(self.params.backend_params.mode, "")
         self.assertEqual(self.params.backend_params.com_if_id, "")
-        def_params = DefaultProcedureParams()
+        def_params = DefaultProcedureParams(None)
         args_to_all_params_tmtc(
             pargs=self.pargs,
             params=self.params,
@@ -63,8 +67,7 @@ def test_basic(self):
         self.assertEqual(self.params.tc_params.apid, 0)
         self.assertEqual(self.params.app_params.use_gui, False)
         self.assertEqual(self.params.app_params.use_ansi_colors, True)
-        self.assertEqual(def_params.service, "17")
-        self.assertEqual(def_params.op_code, "ping")
+        self.assertEqual(def_params.cmd_path, "/PING")
         self.assertEqual(
             self.params.backend_params.mode,
             CoreModeConverter.get_str(CoreModeList.ONE_QUEUE_MODE),
@@ -75,7 +78,7 @@ def test_basic(self):
     def test_delay_set(self):
         self.simple_pargs_cli_set()
         self.pargs.delay = 2.0
-        def_params = DefaultProcedureParams()
+        def_params = DefaultProcedureParams(None)
         args_to_all_params_tmtc(
             pargs=self.pargs,
             params=self.params,
@@ -84,8 +87,7 @@ def test_delay_set(self):
             def_tmtc_params=def_params,
             assign_com_if=False,
         )
-        self.assertEqual(def_params.service, "17")
-        self.assertEqual(def_params.op_code, "ping")
+        self.assertEqual(def_params.cmd_path, "/PING")
         self.assertEqual(self.params.tc_params.delay, 2.0)
 
     def test_cfdp_conversion_basic(self):
@@ -113,7 +115,7 @@ def test_cfdp_conversion_acked(self):
 
     def test_auto_listener_mode(self):
         self.auto_listener_cli_set()
-        def_params = DefaultProcedureParams()
+        def_params = DefaultProcedureParams(None)
         args_to_all_params_tmtc(
             pargs=self.pargs,
             params=self.params,
@@ -123,8 +125,7 @@ def test_auto_listener_mode(self):
             assign_com_if=False,
         )
         self.assertEqual(self.params.backend_params.listener, True)
-        self.assertIsNone(def_params.service)
-        self.assertIsNone(def_params.op_code)
+        self.assertIsNone(def_params.cmd_path)
         self.assertEqual(
             self.params.backend_params.mode,
             CoreModeConverter.get_str(CoreModeList.LISTENER_MODE),
@@ -133,3 +134,85 @@ def test_auto_listener_mode(self):
             self.params.mode, CoreModeConverter.get_str(CoreModeList.LISTENER_MODE)
         )
         self.params = SetupParams()
+
+    def test_tree_printout_conversion_default(self):
+        self.base_cli_set()
+        self.pargs.print_tree = []
+        def_params = DefaultProcedureParams(None)
+        args_to_all_params_tmtc(
+            pargs=self.pargs,
+            params=self.params,
+            hook_obj=self.hook_mock,
+            use_prompts=False,
+            def_tmtc_params=def_params,
+            assign_com_if=False,
+        )
+        self.assertTrue(self.params.app_params.print_tree)
+        self.assertTrue(self.params.app_params.tree_print_with_description)
+        self.assertIsNone(self.params.app_params.tree_print_max_depth)
+
+    def test_tree_printout_conversion_with_custom_args(self):
+        self.base_cli_set()
+        self.pargs.print_tree = ["b", "2"]
+        def_params = DefaultProcedureParams(None)
+        args_to_all_params_tmtc(
+            pargs=self.pargs,
+            params=self.params,
+            hook_obj=self.hook_mock,
+            use_prompts=False,
+            def_tmtc_params=def_params,
+            assign_com_if=False,
+        )
+        self.assertTrue(self.params.app_params.print_tree)
+        self.assertFalse(self.params.app_params.tree_print_with_description)
+        self.assertEqual(self.params.app_params.tree_print_max_depth, 2)
+
+    @patch("builtins.print")
+    def test_tree_printout_0(self, print_mock: MagicMock):
+        root_node_only = CmdTreeNode.root_node()
+        app_params = AppParams()
+        app_params.print_tree = True
+        perform_tree_printout(app_params, root_node_only)
+        self.assertEqual(len(print_mock.call_args_list), 2)
+        self.assertEqual(
+            print_mock.call_args_list[0],
+            call("Printing command tree with full descriptions:"),
+        )
+        printout = print_mock.call_args_list[1].args[0]
+        self.assertTrue("/" in printout)
+        self.assertTrue("[ Root Node ]" in printout)
+
+    @patch("builtins.print")
+    def test_tree_printout_1(self, print_mock: MagicMock):
+        root_node_only = CmdTreeNode.root_node()
+        app_params = AppParams()
+        app_params.print_tree = True
+        app_params.tree_print_with_description = False
+        perform_tree_printout(app_params, root_node_only)
+        self.assertEqual(len(print_mock.call_args_list), 2)
+        self.assertEqual(
+            print_mock.call_args_list[0],
+            call("Printing command tree without descriptions:"),
+        )
+        self.assertEqual(print_mock.call_args_list[1], call(f"/{os.linesep}"))
+
+    @patch("builtins.print")
+    def test_tree_printout_2(self, print_mock: MagicMock):
+        root_node_only = CmdTreeNode.root_node()
+        root_node_only.add_child(CmdTreeNode("acs", "ACS Subsystem"))
+        app_params = AppParams()
+        app_params.print_tree = True
+        app_params.tree_print_with_description = False
+        app_params.tree_print_max_depth = 0
+        perform_tree_printout(app_params, root_node_only)
+        self.assertEqual(len(print_mock.call_args_list), 2)
+        self.assertEqual(
+            print_mock.call_args_list[0],
+            call("Printing command tree without descriptions and maximum depth 0:"),
+        )
+        self.assertEqual(
+            print_mock.call_args_list[1],
+            call(
+                f"/{os.linesep}{TreePart.CORNER.value} ... (cut-off, maximum depth 0){os.linesep}"
+            ),
+        )
diff --git a/tests/config/test_args_parsing.py b/tests/config/test_args_parsing.py
index b2327e3f..39d1324d 100644
--- a/tests/config/test_args_parsing.py
+++ b/tests/config/test_args_parsing.py
@@ -31,14 +31,12 @@ def test_valid_argument_1(self):
     def test_def_proc_argument_empty(self):
         add_default_procedure_arguments(self.arg_parser)
         args = self.arg_parser.parse_args([])
-        self.assertIsNone(args.service)
-        self.assertIsNone(args.op_code)
+        self.assertIsNone(args.cmd_path)
 
     def test_def_proc_argument_valid(self):
         add_default_procedure_arguments(self.arg_parser)
-        args = self.arg_parser.parse_args(["-s", "17", "-o", "ping"])
-        self.assertEqual(args.service, "17")
-        self.assertEqual(args.op_code, "ping")
+        args = self.arg_parser.parse_args(["-p", "/PING"])
+        self.assertEqual(args.cmd_path, "/PING")
 
     def test_generic_arguments_empty(self):
         add_generic_arguments(self.arg_parser)
@@ -61,3 +59,17 @@ def test_com_if_arguments_valid(self):
         add_default_com_if_arguments(self.arg_parser)
         args = self.arg_parser.parse_args(["-c", "udp"])
         self.assertEqual(args.com_if, "udp")
+
+    def test_tree_print_args_0(self):
+        add_default_procedure_arguments(self.arg_parser)
+        args = self.arg_parser.parse_args(["-T"])
+        self.assertEqual(args.print_tree, [])
+        args = self.arg_parser.parse_args(["--print-tree"])
+        self.assertEqual(args.print_tree, [])
+        args = self.arg_parser.parse_args(["--pt"])
+        self.assertEqual(args.print_tree, [])
+
+    def test_tree_print_args_1(self):
+        add_default_procedure_arguments(self.arg_parser)
+        args = self.arg_parser.parse_args(["-T", "b", "2"])
+        self.assertEqual(args.print_tree, ["b", "2"])
diff --git a/tests/config/test_cmd_def_tree.py b/tests/config/test_cmd_def_tree.py
new file mode 100644
index 00000000..b9823265
--- /dev/null
+++ b/tests/config/test_cmd_def_tree.py
@@ -0,0 +1,185 @@
+import os
+from unittest import TestCase
+from tmtccmd.config.tmtc import CmdTreeNode
+
+
+class TestCmdDefTree(TestCase):
+    def setUp(self) -> None:
+        self.cmd_tree = CmdTreeNode.root_node()
+
+    def base_tree(self):
+        self.cmd_tree.add_child(CmdTreeNode("acs", "ACS Subsystem"))
+        self.cmd_tree.add_child(CmdTreeNode("tcs", "TCS Subsystem"))
+
+    def test_state(self):
+        self.base_tree()
+        self.cmd_tree.add_child(CmdTreeNode("ping", "Ping Command"))
+        self.assertEqual(self.cmd_tree.name, "/")
+        self.assertEqual(self.cmd_tree.children["acs"].name, "acs")
+        self.assertEqual(self.cmd_tree.children["acs"].description, "ACS Subsystem")
+        self.assertEqual(self.cmd_tree.children["acs"].children, {})
+        assert self.cmd_tree.children["acs"].parent is not None
+        self.assertEqual(self.cmd_tree.children["acs"].parent.name, "/")
+
+        self.assertEqual(self.cmd_tree.children["tcs"].name, "tcs")
+        self.assertEqual(self.cmd_tree.children["tcs"].description, "TCS Subsystem")
+        self.assertEqual(self.cmd_tree.children["tcs"].children, {})
+        assert self.cmd_tree.children["tcs"].parent is not None
+        self.assertEqual(self.cmd_tree.children["tcs"].parent.name, "/")
+
+        self.assertEqual(self.cmd_tree.children["ping"].name, "ping")
+        self.assertEqual(self.cmd_tree.children["ping"].description, "Ping Command")
+        self.assertEqual(self.cmd_tree.children["ping"].children, {})
+        assert self.cmd_tree.children["ping"].parent is not None
+        self.assertEqual(self.cmd_tree.children["ping"].parent.name, "/")
+
+        self.cmd_tree.children["acs"].add_child(
+            CmdTreeNode("acs_ctrl", "ACS Controller")
+        )
+        self.assertEqual(len(self.cmd_tree.children["acs"].children), 1)
+        acs_ctrl_child = self.cmd_tree.children["acs"].children["acs_ctrl"]
+        self.assertEqual(acs_ctrl_child.name, "acs_ctrl")
+        self.assertEqual(acs_ctrl_child.description, "ACS Controller")
+        assert acs_ctrl_child.parent is not None
+        self.assertEqual(acs_ctrl_child.parent.name, "acs")
+
+    def test_path_contained(self):
+        self.base_tree()
+        self.assertTrue(self.cmd_tree.contains_path("/"))
+
+    def test_path_contained_acs(self):
+        self.base_tree()
+        self.assertTrue(self.cmd_tree.contains_path("/acs"))
+
+    def test_path_contained_tcs(self):
+        self.base_tree()
+        self.assertTrue(self.cmd_tree.contains_path("/tcs"))
+
+    def test_path_contained_acs_ctrl(self):
+        self.base_tree()
+        self.cmd_tree.children["acs"].add_child(
+            CmdTreeNode("acs_ctrl", "ACS Controller")
+        )
+        self.assertTrue(self.cmd_tree.contains_path("/acs/acs_ctrl"))
+
+    def test_named_dict(self):
+        self.base_tree()
+        self.cmd_tree.children["acs"].add_child(
+            CmdTreeNode("acs_ctrl", "ACS Controller")
+        )
+        name_dict = self.cmd_tree.name_dict
+        root_dict = name_dict.get("/")
+        assert root_dict is not None
+        assert "tcs" in root_dict
+        assert root_dict.get("tcs") is None
+        assert "acs" in root_dict
+        acs_dict = root_dict.get("acs")
+        assert acs_dict is not None
+        assert "acs_ctrl" in acs_dict
+        assert acs_dict.get("acs_ctrl") is None
+
+    def test_printout_empty(self):
+        self.assertEqual(str(self.cmd_tree), f"/{os.linesep}")
+
+    def test_prinout_one_sublevel(self):
+        self.base_tree()
+        self.cmd_tree.add_child(CmdTreeNode("ping", "Ping Command"))
+        print(str(self.cmd_tree))
+        self.assertEqual(
+            str(self.cmd_tree),
+            (
+                f"/{os.linesep}"
+                f"├── acs{os.linesep}"
+                f"├── tcs{os.linesep}"
+                f"└── ping{os.linesep}"
+            ),
+        )
+
+    def test_prinout_two_sublevels(self):
+        self.base_tree()
+        self.cmd_tree.add_child(CmdTreeNode("ping", "Ping Command"))
+        self.cmd_tree.children["acs"].add_child(
+            CmdTreeNode("acs_ctrl", "ACS Controller")
+        )
+        print(self.cmd_tree)
+        self.assertEqual(
+            str(self.cmd_tree),
+            (
+                f"/{os.linesep}"
+                f"├── acs{os.linesep}"
+                f"│  └── acs_ctrl{os.linesep}"
+                f"├── tcs{os.linesep}"
+                f"└── ping{os.linesep}"
+            ),
+        )
+
+    def test_printout_two_sublevels_one_cutoff(self):
+        self.base_tree()
+        self.cmd_tree.add_child(CmdTreeNode("ping", "Ping Command"))
+        self.cmd_tree.children["acs"].add_child(
+            CmdTreeNode("acs_ctrl", "ACS Controller")
+        )
+        printout = self.cmd_tree.str_for_tree(False, 1)
+        print(printout)
+        self.assertEqual(
+            printout,
+            (
+                f"/{os.linesep}"
+                f"├── acs{os.linesep}"
+                f"│  └── ... (cut-off, maximum depth 1){os.linesep}"
+                f"├── tcs{os.linesep}"
+                f"└── ping{os.linesep}"
+            ),
+        )
+
+    def test_printout_3(self):
+        self.base_tree()
+        self.cmd_tree.children["acs"].add_child(
+            CmdTreeNode("acs_ctrl", "ACS Controller")
+        )
+        self.cmd_tree.children["tcs"].add_child(
+            CmdTreeNode("tcs_ctrl", "TCS Controller")
+        )
+
+        print(self.cmd_tree)
+        self.assertEqual(
+            str(self.cmd_tree),
+            (
+                f"/{os.linesep}"
+                f"├── acs{os.linesep}"
+                f"│  └── acs_ctrl{os.linesep}"
+                f"└── tcs{os.linesep}"
+                f"   └── tcs_ctrl{os.linesep}"
+            ),
+        )
+
+    def test_printout_4(self):
+        self.base_tree()
+        self.cmd_tree.children["acs"].add_child(
+            CmdTreeNode("acs_ctrl", "ACS Controller")
+        )
+        self.cmd_tree.children["acs"].add_child(CmdTreeNode("mgm_0", "MGM 0"))
+        self.cmd_tree.children["acs"].children["mgm_0"].add_child(
+            CmdTreeNode("update_cfg", "Update Configuration")
+        )
+        self.cmd_tree.children["tcs"].add_child(
+            CmdTreeNode("tcs_ctrl", "TCS Controller")
+        )
+        self.cmd_tree.children["tcs"].add_child(CmdTreeNode("pt1000_0", "PT1000 0"))
+        self.cmd_tree.add_child(CmdTreeNode("ping", "Ping Command"))
+
+        print(self.cmd_tree)
+        self.assertEqual(
+            str(self.cmd_tree),
+            (
+                f"/{os.linesep}"
+                f"├── acs{os.linesep}"
+                f"│  ├── acs_ctrl{os.linesep}"
+                f"│  └── mgm_0{os.linesep}"
+                f"│     └── update_cfg{os.linesep}"
+                f"├── tcs{os.linesep}"
+                f"│  ├── tcs_ctrl{os.linesep}"
+                f"│  └── pt1000_0{os.linesep}"
+                f"└── ping{os.linesep}"
+            ),
+        )
diff --git a/tests/config/test_prompt.py b/tests/config/test_prompt.py
new file mode 100644
index 00000000..aefcdd09
--- /dev/null
+++ b/tests/config/test_prompt.py
@@ -0,0 +1,90 @@
+from unittest import TestCase
+from tmtccmd.config.prompt import prompt_cmd_path
+from tmtccmd.config.tmtc import CmdTreeNode
+from unittest.mock import MagicMock, patch
+
+
+class TestPromptFunc(TestCase):
+    def setUp(self) -> None:
+        self.cmd_tree = CmdTreeNode.root_node()
+        return super().setUp()
+
+    def base_tree(self):
+        self.cmd_tree.add_child(CmdTreeNode("acs", "ACS Subsystem"))
+        self.cmd_tree.add_child(CmdTreeNode("tcs", "TCS Subsystem"))
+        self.cmd_tree.add_child(CmdTreeNode("ping", "Ping Command"))
+
+    @patch("builtins.print")
+    def test_prompt_cmd_path_simple_print(self, _: MagicMock):
+        with patch(
+            "tmtccmd.config.prompt.prompt_toolkit.prompt", side_effect=[":p", "/"]
+        ) as prompt:
+            cmd_path = prompt_cmd_path(self.cmd_tree)
+            prompt.assert_not_called()
+            self.assertEqual(cmd_path, "/")
+
+    @patch("builtins.print")
+    def test_prompt_cmd_path_full_print(self, mocked_print: MagicMock):
+        self.base_tree()
+        with patch(
+            "tmtccmd.config.prompt.prompt_toolkit.prompt", side_effect=[":pf", "acs"]
+        ) as prompt:
+            cmd_path = prompt_cmd_path(self.cmd_tree)
+            self.assertEqual(len(prompt.call_args_list), 2)
+            self.assertEqual(cmd_path, "/acs")
+            call_list = mocked_print.call_args_list
+            self.assertEqual(len(call_list), 2)
+            help_txt = mocked_print.call_args_list[0].args[0]
+            self.assertTrue("Additional commands for prompt:" in help_txt)
+            self.assertTrue(":p Tree Print" in help_txt)
+            self.assertTrue(":r Retry" in help_txt)
+            self.assertTrue(":h Help Text" in help_txt)
+            printout = mocked_print.call_args_list[1].args[0]
+            self.assertTrue("acs" in printout)
+            self.assertTrue("ACS Subsystem" in printout)
+            self.assertTrue("tcs" in printout)
+            self.assertTrue("TCS Subsystem" in printout)
+
+    def test_prompt_cmd_path_retry_explicit(self):
+        self.base_tree()
+        with patch(
+            "tmtccmd.config.prompt.prompt_toolkit.prompt",
+            side_effect=["acs/:r", "ping"],
+        ) as prompt:
+            cmd_path = prompt_cmd_path(self.cmd_tree)
+            self.assertEqual(cmd_path, "/ping")
+            self.assertEqual(len(prompt.call_args_list), 2)
+
+    def test_prompt_cmd_path_retry_prompted(self):
+        self.base_tree()
+        with patch(
+            "tmtccmd.config.prompt.prompt_toolkit.prompt",
+            side_effect=["acss", "acs"],
+        ) as prompt_mock:
+            with patch(
+                "tmtccmd.config.prompt.input",
+                return_value="yes",
+            ) as input_mock:
+                cmd_path = prompt_cmd_path(self.cmd_tree)
+                self.assertEqual(cmd_path, "/acs")
+                self.assertEqual(len(prompt_mock.call_args_list), 2)
+                self.assertEqual(len(input_mock.call_args_list), 1)
+
+    @patch("builtins.print")
+    def test_prompt_help_reprint(self, mocked_print: MagicMock):
+        self.base_tree()
+        with patch(
+            "tmtccmd.config.prompt.prompt_toolkit.prompt", side_effect=[":h", "acs"]
+        ) as prompt:
+            cmd_path = prompt_cmd_path(self.cmd_tree)
+            self.assertEqual(len(prompt.call_args_list), 2)
+            self.assertEqual(cmd_path, "/acs")
+            call_list = mocked_print.call_args_list
+            self.assertEqual(len(call_list), 2)
+            help_txt = mocked_print.call_args_list[0].args[0]
+            self.assertTrue("Additional commands for prompt:" in help_txt)
+            self.assertTrue(":p Tree Print" in help_txt)
+            self.assertTrue(":r Retry" in help_txt)
+            self.assertTrue(":h Help Text" in help_txt)
+            help_txt_2 = mocked_print.call_args_list[1].args[0]
+            self.assertEqual(help_txt, help_txt_2)
diff --git a/tests/hook_obj_mock.py b/tests/hook_obj_mock.py
index 2248dda4..072c6e1c 100644
--- a/tests/hook_obj_mock.py
+++ b/tests/hook_obj_mock.py
@@ -1,13 +1,6 @@
-from abc import abstractmethod
-from typing import Optional
 from unittest.mock import MagicMock
 
-from tmtccmd.com import ComInterface
-from tmtccmd.config.com import ComCfgBase
-from tmtccmd.config.tmtc import TmtcDefinitionWrapper
-from tmtccmd.core.ccsds_backend import CcsdsTmtcBackend
 from tmtccmd.config import HookBase
-from tmtccmd.util.obj_id import ObjectIdDictT
 
 
 def create_hook_mock() -> HookBase:
@@ -16,67 +9,3 @@ def create_hook_mock() -> HookBase:
     """
     tmtc_hook_base = MagicMock(spec=HookBase)
     return tmtc_hook_base
-
-
-def create_hook_mock_with_srv_handlers() -> HookBase:
-    tmtc_hook_base = create_hook_mock()
-    tmtc_hook_base.handle_service_8_telemetry = MagicMock(return_value=(["Test"], [0]))
-    # Valid returnvalue for now
-    srv_3_return_tuple = (["Test"], [0], bytearray(0b10000000), 1)
-    tmtc_hook_base.handle_service_3_housekeeping = MagicMock(
-        return_value=srv_3_return_tuple
-    )
-    return tmtc_hook_base
-
-
-class TestHookObj(HookBase):
-    service_8_handler_called = False
-    service_5_handler_called = False
-    service_3_handler_called = False
-
-    def __init__(self):
-        super().__init__()
-        self.get_obj_id_called = False
-        self.add_globals_pre_args_parsing_called = False
-        self.add_globals_post_args_parsing_called = False
-        self.assign_communication_interface_called = False
-
-    @abstractmethod
-    def get_object_ids(self) -> ObjectIdDictT:
-        """The user can specify an object ID dictionary here mapping object ID bytearrays to a
-        list. This list could contain containing the string representation or additional
-        information about that object ID.
-        """
-        return super().get_object_ids()
-
-    @abstractmethod
-    def assign_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
-        """Assign the communication interface used by the TMTC commander to send and receive
-        TMTC with.
-
-        :param com_if_key:      String key of the communication interface to be created.
-        """
-        from tmtccmd.config.com import create_com_interface_default
-
-        com_cfg = ComCfgBase(com_if_key, self.cfg_path)
-        return create_com_interface_default(com_cfg)
-
-    @abstractmethod
-    def get_tmtc_definitions(self) -> TmtcDefinitionWrapper:
-        """This is a dicitonary mapping services represented by strings to an operation code
-        dictionary.
-
-        :return:
-        """
-        from tmtccmd.config.globals import get_default_tmtc_defs
-
-        return get_default_tmtc_defs()
-
-    @abstractmethod
-    def perform_mode_operation(self, tmtc_backend: CcsdsTmtcBackend, mode: int):
-        """Perform custom mode operations
-        :param tmtc_backend:
-        :param mode:
-        :return:
-        """
-        pass
diff --git a/tests/test_backend.py b/tests/test_backend.py
index 460ef1b6..9c47482f 100644
--- a/tests/test_backend.py
+++ b/tests/test_backend.py
@@ -33,8 +33,7 @@ def __init__(self):
             seq_cnt_provider=None,
         )
         self.send_cb_call_args: Optional[SendCbParams] = None
-        self.send_cb_service_arg: Optional[str] = None
-        self.send_cb_op_code_arg: Optional[str] = None
+        self.send_cb_cmd_path_arg: Optional[str] = None
 
     def send_cb(self, send_params: SendCbParams):
         self.send_cb_call_count += 1
@@ -46,36 +45,26 @@ def queue_finished_cb(self, info: TcProcedureBase):
     def feed_cb(self, info: ProcedureWrapper, wrapper: FeedWrapper):
         self.queue_helper.queue_wrapper = wrapper.queue_wrapper
         self.feed_cb_call_count += 1
-        self.send_cb_service_arg = None
+        self.send_cb_cmd_path_arg = None
         self.send_cb_op_code_arg = None
         if info is not None:
             if info.proc_type == TcProcedureType.DEFAULT:
                 self.feed_cb_def_proc_count += 1
                 def_info = info.to_def_procedure()
-                if def_info.service != "17":
+                if def_info.cmd_path != "/ping":
                     self.is_feed_cb_valid = False
-                self.send_cb_service_arg = def_info.service
-                self.send_cb_op_code_arg = def_info.op_code
-                if def_info.service == "17":
-                    if def_info.op_code == "0":
-                        self.queue_helper.add_pus_tc(
-                            PusTelecommand(service=17, subservice=1)
-                        )
-                    elif def_info.op_code == "1":
-                        self.queue_helper.add_pus_tc(
-                            PusTelecommand(service=17, subservice=1)
-                        )
-                        self.queue_helper.add_pus_tc(
-                            PusTelecommand(service=5, subservice=1)
-                        )
-                    elif def_info.op_code == "2":
-                        self.queue_helper.add_pus_tc(
-                            PusTelecommand(service=17, subservice=1)
-                        )
-                        self.queue_helper.add_wait(timedelta(milliseconds=20))
-                        self.queue_helper.add_pus_tc(
-                            PusTelecommand(service=5, subservice=1)
-                        )
+                self.send_cb_cmd_path_arg = def_info.cmd_path
+                if def_info.cmd_path == "/ping":
+                    self.queue_helper.add_pus_tc(
+                        PusTelecommand(service=17, subservice=1)
+                    )
+                elif def_info.cmd_path == "/event":
+                    self.queue_helper.add_pus_tc(
+                        PusTelecommand(service=17, subservice=1)
+                    )
+                    self.queue_helper.add_pus_tc(
+                        PusTelecommand(service=5, subservice=1)
+                    )
 
 
 class TestBackend(TestCase):
@@ -127,7 +116,7 @@ def test_basic_ops(self):
         self.backend.tc_mode = TcMode.ONE_QUEUE
         with self.assertRaises(NoValidProcedureSet):
             self.backend.periodic_op()
-        self.backend.current_procedure = DefaultProcedureInfo(service="17", op_code="0")
+        self.backend.current_procedure = DefaultProcedureInfo(cmd_path="/ping")
 
         res = self.backend.periodic_op()
         # Only one queue entry which is handled immediately
@@ -135,7 +124,7 @@ def test_basic_ops(self):
         self.assertEqual(self.tc_handler.feed_cb_def_proc_count, 1)
         self.assertEqual(self.tc_handler.feed_cb_call_count, 1)
         self.assertEqual(self.tc_handler.send_cb_call_count, 1)
-        self.assertIsNotNone(self.tc_handler.send_cb_call_args)
+        assert self.tc_handler.send_cb_call_args is not None
         self.assertIsNotNone(self.tc_handler.send_cb_call_args.info)
         self.assertIsNotNone(self.tc_handler.send_cb_call_args.entry)
         self.assertEqual(self.tc_handler.send_cb_call_args.com_if, self.com_if)
@@ -148,7 +137,7 @@ def test_basic_ops(self):
     def test_one_queue_multi_entry_ops(self):
         self.backend.tm_mode = TmMode.IDLE
         self.backend.tc_mode = TcMode.ONE_QUEUE
-        self.backend.current_procedure = DefaultProcedureInfo(service="17", op_code="1")
+        self.backend.current_procedure = DefaultProcedureInfo(cmd_path="/event")
         res = self.backend.periodic_op()
         self.assertEqual(res.request, BackendRequest.CALL_NEXT)
         self.assertEqual(self.tc_handler.feed_cb_def_proc_count, 1)
@@ -165,7 +154,7 @@ def test_one_queue_multi_entry_ops(self):
     def test_multi_queue_ops(self):
         self.backend.tm_mode = TmMode.IDLE
         self.backend.tc_mode = TcMode.MULTI_QUEUE
-        self.backend.current_procedure = DefaultProcedureInfo(service="17", op_code="0")
+        self.backend.current_procedure = DefaultProcedureInfo(cmd_path="/ping")
         res = self.backend.periodic_op()
         self.assertEqual(res.request, BackendRequest.CALL_NEXT)
         self.assertEqual(self.backend.request, BackendRequest.CALL_NEXT)
@@ -178,7 +167,7 @@ def test_multi_queue_ops(self):
         self.assertEqual(self.tc_handler.feed_cb_call_count, 1)
         self.assertEqual(res.request, BackendRequest.DELAY_IDLE)
         self.backend.tc_mode = TcMode.MULTI_QUEUE
-        self.backend.current_procedure = DefaultProcedureInfo(service="17", op_code="0")
+        self.backend.current_procedure = DefaultProcedureInfo(cmd_path="/ping")
         res = self.backend.periodic_op()
         self.assertEqual(res.request, BackendRequest.CALL_NEXT)
         self.assertEqual(self.backend.request, BackendRequest.CALL_NEXT)
@@ -186,7 +175,7 @@ def test_multi_queue_ops(self):
         self.assertEqual(self.tc_handler.feed_cb_call_count, 2)
 
     def test_procedure_handling(self):
-        def_proc = DefaultProcedureInfo(service="17", op_code="0")
+        def_proc = DefaultProcedureInfo(cmd_path="/ping")
         self.backend.current_procedure = def_proc
         self.assertEqual(
             self.backend.current_procedure.proc_type, TcProcedureType.DEFAULT
@@ -194,8 +183,7 @@ def test_procedure_handling(self):
         proc_helper = self.backend.current_procedure
         def_proc = proc_helper.to_def_procedure()
         self.assertIsNotNone(def_proc)
-        self.assertEqual(def_proc.service, "17")
-        self.assertEqual(def_proc.op_code, "0")
+        self.assertEqual(def_proc.cmd_path, "/ping")
 
     def _check_tc_req_recvd(self, service: int, subservice: int):
         self.assertEqual(self.tc_handler.send_cb_call_args.com_if, self.com_if)
diff --git a/tests/test_seq_sender.py b/tests/test_seq_sender.py
index 9c64e063..616aa1f6 100644
--- a/tests/test_seq_sender.py
+++ b/tests/test_seq_sender.py
@@ -45,7 +45,7 @@ def test_basic(self):
         self.tc_handler_mock.send_cb.assert_called_with(ANY)
         call_args = self.tc_handler_mock.send_cb.call_args
         send_cb_params = cast(SendCbParams, call_args.args[0])
-        self.assertEqual(send_cb_params.info.base, DefaultProcedureInfo.empty())
+        self.assertEqual(send_cb_params.info.procedure, DefaultProcedureInfo.empty())
         raw_tc_entry = send_cb_params.entry.to_raw_tc_entry()
         self.assertEqual(raw_tc_entry.tc, bytes([0, 1, 2]))
         # Queue should be empty now
@@ -124,7 +124,7 @@ def test_interpacket_delay(self):
         call_args = self.tc_handler_mock.send_cb.call_args
         send_cb_params = cast(SendCbParams, call_args.args[0])
         self.assertEqual(send_cb_params.com_if, self.com_if)
-        self.assertIsNone(send_cb_params.info.base)
+        self.assertIsNone(send_cb_params.info.procedure)
         self.assertEqual(send_cb_params.entry.to_pus_tc_entry().pus_tc, ping_cmd)
         res = self.seq_sender.operation(self.com_if)
         self.assertFalse(res.tc_sent)
@@ -132,7 +132,7 @@ def test_interpacket_delay(self):
         self.tc_handler_mock.send_cb.assert_called_with(ANY)
         call_args = self.tc_handler_mock.send_cb.call_args
         send_cb_params = cast(SendCbParams, call_args.args[0])
-        self.assertIsNone(send_cb_params.info.base)
+        self.assertIsNone(send_cb_params.info.procedure)
         self.assertEqual(
             send_cb_params.entry.to_packet_delay_entry().delay_time.total_seconds(),
             inter_packet_delay.total_seconds(),
diff --git a/tmtccmd/pus/tc/s17_test.py b/tmtccmd/pus/tc/s17_test.py
index dcc354ad..07f23c4e 100644
--- a/tmtccmd/pus/tc/s17_test.py
+++ b/tmtccmd/pus/tc/s17_test.py
@@ -1,14 +1,11 @@
-import deprecation
-
-from tmtccmd.version import get_version
-from spacepackets.ecss import PusTelecommand, PusService
+from deprecated.sphinx import deprecated
+from spacepackets.ecss import PusService, PusTelecommand
 from spacepackets.ecss.pus_17_test import Subservice
 
 
-@deprecation.deprecated(
-    deprecated_in="4.0.0a2",
-    current_version=get_version(),
-    details="use create... API instead",
+@deprecated(
+    version="4.0.0a2",
+    reason="use create... API instead",
 )
 def pack_service_17_ping_command(apid: int = 0, seq_count: int = 0) -> PusTelecommand:
     return create_service_17_ping_command(apid, seq_count)