Client app Tools

This chapter will show how to setup a client app.

Tools

This tutorial will show you some helpful tools to build a Motorcortex python client application. These tools are built on top of motorcortex-python and simplify common tasks such as setting up a client from scratch, adding new parameters like buttons, deploying to the Motorcortex application.

The first Client App Template tab will guide you through the installation and usage of a ready-to-use client application template that can be used as a starting point for your own client applications. The subsequent tabs will introduce additional tools and utilities that can enhance your client application development experience and will build upon the provided template. However, using the template is not a requirement to use the other principles shown in the other tabs.


Download of the Client Application Template

For the ease of use, a client application template is provided that sets up a basic Motorcortex-python client application. This template can be used as a starting point for your own client applications. It includes the necessary connection setup, parameter tree initialization, and provides a structure for running functions.

You can find the client application template on GitHub.

To use the template, clone the repository:

git clone hhttps://git.vectioneer.com/pub/mcx-client-app-template.git

The file mcx-client-app.py is the main entry point for the client application. You can edit this file to add your own functionality and logic. The next tab will guide you through the mcx-client-app.py file and show how to extend it for your own use cases.

Structure of the Template

The structure of the template project is as follows:

mcx-client-app-template/
├── mcx-client-app.py                 # Main Python client app template for Motorcortex integration
├── mcx-client-app-legacy.py          # Legacy version of the main Python client app template
│
├── makedeb.sh                        # Script to build a Debian package for MCX-RTOS deployment
├── collect_python_dependencies.sh    # Script to collect non-pure Python dependencies for packaging
│
├── template.service.in               # Template for the systemd service file
├── README.md                         # Project documentation and usage instructions
│
├── build/                            # Build artifacts and generated Debian package files
│
├── examples/                         # Example scripts demonstrating usage
│   ├── robot_app.py                  # Example: robot control application
│   └── start_button.py               # Example: start button integration
├── src/                              # Source code for the client app
│   └── mcx_client_app/
│       ├── __init__.py               # Package initializer
│       ├── app.py                    # Main application logic
│       └── mcx_client_app.egg-info/  # Metadata for the Python package

The main application logic for the client app is contained in src/mcx_client_app and can thus be easily imported as a module in your own projects. Additional packages can be added to the src folder as needed for your own applications.


mcx-client-app.py

To create everything you want, the main file to edit is mcx-client-app.py. This file contains the main logic of the client application and is where you can add your own functionality and logic. It has been made easy to extend and modify according to your needs.

The bare minimum to get started is as follows, do not forget to replace the login, password and target_url with your own values:

from src.mcx_client_app import McxClientApp, MCXClientAppOptions # Import the main client app class and options

class MyClientApp(McxClientApp):
    def __init__(self, options: MCXClientAppOptions):
        super().__init__(options)  # Initialize the base class with the provided options

    def action(self):
        print("Client app is running...")  # Define the action to be performed when the client app runs

if __name__ == "__main__":
    client_options = MCXClientAppOptions(
        login="",
        password="",
        target_url=""
    )

    app = MyClientApp(client_options)  # Create an instance of your client app with the specified options
    app.run()  # Run the client app with the specified

When you run this code, it creates a basic Motorcortex-python client application that connects to your specified Motorcortex server. By default, since no custom logic is added yet, the application will simply connect and repeatedly print “Client app is running…” to the console.

The run method manages the entire lifecycle of the connection: it connects to the Motorcortex server, engages the robot if needed, and then continuously calls your action method in a loop. When you stop the application, it will automatically disengage the robot and disconnect safely from the server.

To build your own functionality, you should implement the action method. This is where you can read parameters, send commands, or add any custom logic your client needs. You can also override other methods from the McxClientApp class to further customize your application’s behavior.

  • Use the startOp method to add logic that should run once when the client starts (for example, initializing parameters or setting up subscriptions).
  • Use the onExit method to add cleanup logic that should run when the client stops (such as saving state or releasing resources).

Only the action method is required; startOp and onExit are optional and can be implemented as needed.

Subscribing to Parameters

To subscribe to parameters and receive updates, you can use the usual self.sub.subscribe. However, it is important to note that subscriptions should be created in the startOp method, which is called once after the connection is established. This ensures that the subscriptions are set up correctly and ready to receive updates. Additionally, you can use the notify method on the subscription to define a callback function that will be executed whenever the subscribed parameter changes but this happens in a different thread. Thus, be cautious when accessing shared data between threads.

The class ThreadSafeValue can be used to safely share data between the subscription callback and the main thread where action runs. This class has the methods get() and set(value) to access and modify the value in a thread-safe manner.

Running action in a Separate Thread: McxClientAppThread

If your client application needs to perform long-running or blocking operations in the action method, you can use the McxClientAppThread class instead of McxClientApp. With McxClientAppThread, the action method runs in its own thread, while the main thread continues to manage the connection and other logic. This separation allows your application to remain responsive, even if action takes time to complete.

To safely share data between the main thread and the action thread, use the provided ThreadSafeValue class. This utility ensures that data access is synchronized, preventing race conditions and ensuring data integrity.

Example of using McxClientAppThread with ThreadSafeValue:

from src.mcx_client_app import McxClientAppThread, McxClientAppOptions, ThreadSafeValue

class ToolPoseMonitorApp(McxClientAppThread):
    """
    Application that monitors and prints the tool pose.
    """
    def __init__(self, options: McxClientAppOptions):
        super().__init__(options)
        self.toolPoseSubscription = None
        self.toolPose: ThreadSafeValue[list[float]] = ThreadSafeValue([0, 0, 0, 0, 0, 0])
    
    def startOp(self) -> None:
        """
        Subscribe to tool pose updates.
        """
        self.toolPoseSubscription = self.sub.subscribe(
            'root/ManipulatorControl/manipulatorToolPoseActual', 
            "Group1", 
            frq_divider=1000
        )
        self.toolPoseSubscription.notify(lambda values: self.toolPose.set(values[0].value))
    
    def action(self) -> None:
        """
        Print current tool pose.
        """
        print(f"Toolpose {self.toolPose.get()}")
        self.wait(1)

if __name__ == "__main__":
    client_options = McxClientAppOptions(
        login="",
        password="",
        target_url="",
    )

    app = ToolPoseMonitorApp(client_options)
    app.run()

In this example, the application subscribes to the tool pose parameter and prints its value every second. The ThreadSafeValue class is used to safely share the tool pose data between threads. The startOp method sets up the subscription after the connection is established, and the action method handles the periodic printing in its own thread.


Start button

You can easily add a start button to your Motorcortex-python client application using the start_stop_param parameter in MCXClientAppOptions. This button allows you to start and stop your client application directly from the Motorcortex GUI. When the button is pressed, the action method of your client application will be executed. Pressing the button again will stop the execution of the action method.

  1. Firstly, add the parameter to the tree in your Motorcortex application. You can do this by adding a boolean parameter to .conf/user/parameters.json in the motorcortex portal. For example, you can add the following parameter add the bottom of the file:
{
      "Name": "GUI",
      "Children": [
        {
          "Name": "PythonScript01",
          "Children": [
            {
              "Name": "StartStop",
              "Type": "bool, input",
              "Value": 0
            }
          ]
        }
      ]
    }
  1. Next, in your mcx-client-app.py, set the start_stop_param option in MCXClientAppOptions to the path of the start button parameter you just created. For example:
client_options = MCXClientAppOptions(
    login="",
    password="",
    target_url="",
    start_stop_param="root/UserParameters/GUI/PythonScript01/StartStop" # Path to the start button parameter
)
  1. Add a switch in your GUI to interact with the start button parameter or use the DESK-Tool to toggle the button by manually changing its value.

Other Custom buttons

Other buttons with other logic can be added in a similar way by creating boolean parameters in the Motorcortex application and reading their values in the action method of your client application or by creating a subscription in startOp method. You can implement custom logic based on the state of these buttons to control various aspects of your client application.

For example, you can create a button to trigger a reset to change values:

from src.mcx_client_app import McxClientApp, McxClientAppOptions, ThreadSafeValue

class CustomButtonApp(McxClientApp):
    """
    Application that monitors and prints the tool pose.
    """
    def __init__(self, options: McxClientAppOptions):
        super().__init__(options)
        self.buttonSubscription = None
        self.counter: int = 0
        self.__reset: ThreadSafeValue[bool] = ThreadSafeValue(False)
    
    def startOp(self) -> None:
        """
        Subscribe to button updates.
        """
        self.buttonSubscription = self.sub.subscribe(
            'root/UserParameters/GUI/PythonScript01/resetButton', 
            "Group1", 
            frq_divider=10 
        )
        self.buttonSubscription.notify(self.__button_callback)
        self.req.setParameter('root/UserParameters/GUI/PythonScript01/Counter', 0).get()
    
    def __button_callback(self, msg) -> None:
        """Callback for button press - only trigger on rising edge (0 -> 1). (Happens in the subscription thread)"""
        value = msg[0].value[0]
        if value != 0:  # Button pressed (rising edge)
            self.__reset.set(True)

    def __reset_counter(self) -> None:
        self.counter = 0
        self.__reset.set(False)
        self.req.setParameter('root/UserParameters/GUI/PythonScript01/resetButton', 0).get()
    
    def action(self) -> None:
        """
        Increment counter and check for reset button press.
        """
        if self.__reset.get():
            self.__reset_counter()
            print("Counter reset!")
        else:
            self.counter += 1
            print(f"Counter: {self.counter}")
            self.req.setParameter('root/UserParameters/GUI/PythonScript01/Counter', self.counter).get()
        
        self.wait(1)  # Wait 1 second between increments

if __name__ == "__main__":
    client_options = McxClientAppOptions(
        login="",
        password="",
        target_url="",
    )

    app = CustomButtonApp(client_options)
    app.run()

For this example, the parameter root/UserParameters/GUI/PythonScript01/resetButton should be created as a boolean input parameter in the Motorcortex application abd . When the button is pressed, the counter will reset to zero, and the button will be set back to 0 after the reset action is performed.


Packaging your Client Application for Deployment

Instead of running the client application on a seperate pc that connects to the Motorcortex server over the network, you can also deploy the client application directly to the Motorcortex application. This can be useful for standalone setups or when you want to ensure that the client application is always running alongside the Motorcortex server.

A convenient file, makeDeb.sh, has been created to help you package your client application into a .deb file, which can then be installed on the Motorcortex application. This script automates the process of creating the necessary directory structure and files for a Debian package.

There are a few steps to follow to successfully create and deploy your client application:

  1. Ensure that your client application is working correctly on your local machine. Test it thoroughly to make sure it behaves as expected.

  2. Modify the makeDeb.sh script to include the correct paths and names for your client application. This script automates the process of creating the necessary directory structure and files for a Debian package, making it easy to package your client application into a .deb file for installation on the Motorcortex system.

    # Set variables
    PACKAGE_NAME="mcx-client-app"
    VERSION="1.0"
    ARCHITECTURE="all"
    MAINTAINER="Vectioneer B.V. <info@vectioneer.com>"
    DESCRIPTION="A client application for getting and setting data in a Motorcortex Control Application"
    PYTHON_SCRIPT="Test.py"
    PYTHON_MODULES="src" # directories, space separated list
    SERVICE="${PACKAGE_NAME}"
    BUILDFOLDER="build"
    

    Make sure to replace PYTHON_SCRIPT with the name of your main client application script and PYTHON_MODULES with any additional directories that contain modules your application depends on. As the client template McxClientApp is in the src folder, keep this folder in the list.

  3. Run the makeDeb.sh script to create the .deb package. This will generate a file named mcx-client-app_1.0.deb (or similar, depending on your name and version).

  4. Upload the generated .deb file to your Motorcortex binaries in the portal and deploy!

  5. After deployment, your client application should be installed and ready to run on the Motorcortex system. As it runs as a service, you can manage it and look at the logs in Cockpit under Services.

Additional Python packages

One of the powers of using Python for your client application is the vast ecosystem of available packages. A few packages are preinstalled on the Motorcortex application, but if your client application depends on additional packages, you must include them in your deployment package.

Click to expand: Default Python packages installed on the Motorcortex application
Package Version
btrfsutil 5.16.2
cairocffi 1.1.0
CairoSVG 2.4.2
cffi 1.15.0
cssselect2 0.3.0
cycler 0.11.0
defusedxml 0.7.1
html5lib 1.1
Jinja2 3.1.4
lxml 4.8.0
MarkupSafe 2.1.1
matplotlib 3.5.1
motorcortex-python 0.22.9
motorcortex-python-tools 1.4.3
numpy 1.22.3
pandas 1.4.2
Pillow 9.4.0
pip 22.0.3
ply 3.11
protobuf 3.20.3
pycparser 2.21
pynng 0.7.1
pyparsing 3.0.7
Pyphen 0.9.5
python-dateutil 2.8.2
pytz 2022.1
setuptools 59.5.0
six 1.16.0
smbus 1.1
sniffio 1.2.0
tinycss2 1.0.2
UNKNOWN 0.0.0
WeasyPrint 51
webencodings 0.5.1

A convenient way to include additional packages to the .deb package so they can be deployed with your client application is given below:

  1. Edit the script collect_python_dependencies.sh. Put in DepsLatest= the list of additional python packages you want to include.

  2. Transfer the script to a location accessible by the Motorcortex application, for example using scp:

        scp collect_python_dependencies.sh user@remote # On your local machine
    
  3. ssh into your Motorcortex application or use a local development setup with the same OS as the Motorcortex application. Make sure internet access is available. For instructions on configuring the network, see Network Configuration.

  4. Run the collect_python_dependencies.sh script on the Motorcortex application. This will create a .tar.gz file containing the specified python packages and their dependencies with an unique name in the form of dependencies.XXXXXXXXXX.tar.gz. Take note of this name for the next step.

        ./collect_python_dependencies.sh
    
  5. Copy the generated .tar.gz file back to your development machine.

        scp user@remote:dependencies.XXXXXXXXXX.tar.gz . # On your local machine
    
  6. Modify the makeDeb.sh script to include the generated .tar.gz file in the package. You can do this by extracting the contents of the .tar.gz file into the folder VENV_REQ_DIR="" that is chosen in the makeDeb.sh script. This way, when the .deb package is installed on the Motorcortex application, the additional python packages will be available for your client application to use.

The next steps are the same as before: run the makeDeb.sh script to create the .deb package, upload it to your Motorcortex binaries in the portal, and deploy it to your Motorcortex application.