From d49aaf96de54001af33e5a6acb5965a2f0201ca9 Mon Sep 17 00:00:00 2001 From: Rik Huygen Date: Thu, 27 Feb 2025 23:09:48 +0100 Subject: [PATCH] first batch of API docs for cgse-common --- api/bits/index.html | 230 +++++++++++++++++++++++++++++++++++---- search/search_index.json | 2 +- 2 files changed, 212 insertions(+), 20 deletions(-) diff --git a/api/bits/index.html b/api/bits/index.html index d8a5366..f29590a 100644 --- a/api/bits/index.html +++ b/api/bits/index.html @@ -2669,12 +2669,77 @@

-

Set the given bits in value to 0. -Args: - value (int): the value where the given bits shall be changed - bits (tuple): a tuple with start and stop bits -Returns: - the changed value

+

Set the given bits in value to 0.

+ + +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ value + + int + +
+

the value where the given bits shall be changed

+
+
+ required +
+ bits + + tuple + +
+

a tuple with start and stop bits

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ int + +
+

the changed value

+
+
@@ -2696,13 +2761,6 @@

Calculate the checksum for (part of) the data.

-
- Reference -

The description of the CRC calculation for RMAP is given in the ECSS document -Space Engineering: SpaceWire - Remote Memory Access Protocol, section A.3 -on page 80 [ECSS‐E‐ST‐50‐52C].

-
-

Parameters:

@@ -2787,6 +2845,13 @@

+ +
+ Reference +

The description of the CRC calculation for RMAP is given in the ECSS document +Space Engineering: SpaceWire - Remote Memory Access Protocol, section A.3 +on page 80 [ECSS‐E‐ST‐50‐52C].

+
@@ -3058,6 +3123,37 @@

Return the signed equivalent of a hex or binary number.

+

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ value + + int + +
+

an integer value.

+
+
+ required +
+ +

Returns:

@@ -3125,6 +3221,37 @@

Return the signed equivalent of a hex or binary number.

+

Parameters:

+

+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ value + + int + +
+

an integer value.

+
+
+ required +
+ +

Returns:

@@ -3264,12 +3391,77 @@

-

Set the given bits in value to 1. -Args: - value (int): the value where the given bits shall be changed - bits (tuple): a tuple with start and stop bits -Returns: - the changed value

+

Set the given bits in value to 1.

+ + +

Parameters:

+

+ + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
+ value + + int + +
+

the value where the given bits shall be changed

+
+
+ required +
+ bits + + tuple + +
+

a tuple with start and stop bits

+
+
+ required +
+ + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ int + +
+

the changed value.

+
+
diff --git a/search/search_index.json b/search/search_index.json index a8c3e3b..7421452 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"

Tip

See the navigation links in the header or side-bar.

Click (top left) on mobile.

"},{"location":"#welcome","title":"Welcome","text":"

Welcome to the CGSE framework documentation.

Get started or go straight to the Tutorial

"},{"location":"#what-is-the-cgse","title":"What is the CGSE?","text":"

The CGSE is short for Common-EGSE, a framework for managing and running test equipment in a lab. The EGSE stands for Electrical Ground Support Equipment, and this includes all equipment that is used to test or calibration an instrument.

"},{"location":"getting_started/","title":"Getting started","text":"

All you need to get started using and building the CGSE.

"},{"location":"getting_started/#requirements","title":"Requirements","text":""},{"location":"getting_started/#virtual-environment","title":"Virtual environment","text":"

You should always work inside a virtual environment to somehow containerize your project such that it doesn't pollute your global environment and you can run different projects next to each other. Create and activate a new virtual environment as follows. It should be Python >= 3.9

$ python -m venv venv\n$ source venv/bin/activate\n
"},{"location":"getting_started/#installation","title":"Installation","text":"

The easiest way to install the CGSE is to use the pip command. Since the CGSE is a monorepo and consists of several packages, you will need to make your choice which package you need for your project. You can however start with the cgse-common which contains all common code that is generic and useful as a basis for other packages.

$ pip install cgse-common\n

Check the list of packages that are part of the CGSE repo and can be installed with pip. The packages are described in more detail in the sections Libs and Projects.

"},{"location":"getting_started/#set-up-your-environment","title":"Set up your environment","text":"

To check your installation and set up your environment, here are a few tips.

The version of the core packages and any plugin packages can be verified as follows. The version you installed will probably be higher and more lines will appear when other packages are installed.

$ py -m egse.version\nCGSE version in Settings: 0.5.0\nInstalled version for cgse-common= 0.5.0\n

Check your environment with the command below. This will probably print out some warning since you have not defined the expected environment variables yet. There are two mandatory environment variables: PROJECT and SITE_ID. The former shall contain the name of your project without spaces and preferably a single word or an acronym like PLATO, ARIEL, MARVEL, MERCATOR. The latter is the name of the site or lab where the tests will be performed. Good names are KUL, ESA, LAB23.

The other environment variables follow the pattern <PROJECT>_..., i.e. they all start with the project name as defined in the PROJECT environment variable. You should define at least <PROJECT>_DATA_STORAGE_LOCATION. The configuration data and log file location will be derived from it unless they are explicitly set themselves.

Let's define the three expected environment variables:

$ export PROJECT=ARIEL\n$ export SITE_ID=VACUUM_LAB\n$ export ARIEL_DATA_STORAGE_LOCATION=~/data\n

Rerunning the above command now gives:

$ py -m egse.env\nEnvironment variables:\n    PROJECT = ARIEL\n    SITE_ID = VACUUM_LAB\n    ARIEL_DATA_STORAGE_LOCATION = /Users/rik/data\n    ARIEL_CONF_DATA_LOCATION = not set\n    ARIEL_CONF_REPO_LOCATION = not set\n    ARIEL_LOG_FILE_LOCATION = not set\n    ARIEL_LOCAL_SETTINGS = not set\n\nGenerated locations and filenames\n    get_data_storage_location() = '/Users/rik/data/VACUUM_LAB'  \u27f6 ERROR: The data storage location doesn't exist!\n    get_conf_data_location() = '/Users/rik/data/VACUUM_LAB/conf'  \u27f6 ERROR: The configuration data location doesn't exist!\n    get_conf_repo_location() = None  \u27f6 ERROR: The configuration repository location doesn't exist!\n    get_log_file_location() = '/Users/rik/data/VACUUM_LAB/log'  \u27f6 ERROR: The log files location doesn't exist!\n    get_local_settings() = None  \u27f6 ERROR: The local settings file is not defined or doesn't exist!\n\nuse the '--full' flag to get a more detailed report, '--doc' for help on the variables.\n

Note

The folders that do not exist (and are not None) can be created by adding the option --mkdir to the above command.

"},{"location":"getting_started/#check-your-settings","title":"Check your Settings","text":"

Settings contains the static configuration of your system, test setup, equipment, including the System Under Test (SUT). You can test your settings with the command below. Let's first also set the ARIEL_LOCALSETTINGS environment variables:

$ export ARIEL_LOCAL_SETTINGS=~/cgse/local_settings_ariel_vacuum_lab.yaml\n$ py -m egse.settings\nSettings\n\u251c\u2500\u2500 PACKAGES\n\u2502   \u2514\u2500\u2500 CGSE_COMMON: Common classes, functions, decorators, etc. for the CGSE\n\u251c\u2500\u2500 SITE\n\u2502   \u251c\u2500\u2500 ID: VACUUM_LAB\n\u2502   \u251c\u2500\u2500 SSH_SERVER: localhost\n\u2502   \u2514\u2500\u2500 SSH_PORT: 22\n\u2514\u2500\u2500 PROCESS\n    \u2514\u2500\u2500 METRICS_INTERVAL: 10\nMemoized locations:\n['/Users/rik/tmp/gettings-started/venv/lib/python3.9/site-packages/cgse_common/settings.yaml', \n'/Users/rik/cgse/local_settings_ariel_vacuum_lab.yaml']\n
These Settings will grow when you add more packages to your installation or when you define settings yourself in the local settings file.

"},{"location":"help/","title":"Help","text":"

The best way to get help for something that you couldn't find in the documentation on this site, is to contact one of the authors.

"},{"location":"help/#bugs-and-feature-requests","title":"Bugs and Feature requests","text":"

Report any bugs or issues through GitHub on the CGSE issues page.

"},{"location":"initialize/","title":"Initialize your project","text":"

So, we have seen how to get started with some basic commands and only the cgse-common package. It's time now to initialize your project properly with all the necessary services.

"},{"location":"initialize/#set-up-your-environment","title":"Set up your environment","text":"

I assume you are in the same environment that we have set up in the previous section where also the cgse-common package was installed. We will install another package that will provide us with the full functionality of the cgse command. Install the cgse-tools and cgse-core packages which depends on cgse-core and will also install that package.

$ pip install cgse-tools cgse-core\n

You should now have at least the follow three packages installed in your virtual environment:

$ pip list | grep cgse\ncgse-common       0.5.0\ncgse-core         0.5.0\ncgse-tools        0.5.0\n
"},{"location":"initialize/#the-cgse-command","title":"The cgse command","text":"

The two new packages that have been installed (cgse-core and cgse-tools) provide the cgse command that we will use to initialize your environment, but this command is also used to inspect different parts of the system, manage core services and device drivers, etc.

When you run the cgse command without any arguments, it will show something like this:

$ cgse\n\n Usage: cgse [OPTIONS] COMMAND [ARGS]...\n\n The main cgse command to inspect, configure, monitor the core services and device control servers.\n\n\u256d\u2500 Options \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 --install-completion          Install completion for the current shell.                                                                                      \u2502\n\u2502 --show-completion             Show completion for the current shell, to copy it or customize the installation.                                               \u2502\n\u2502 --help                        Show this message and exit.                                                                                                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\u256d\u2500 Commands \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 version   Prints the version of the cgse-core and other registered packages.                                                                                 \u2502\n\u2502 init      Initialize your project.                                                                                                                           \u2502\n\u2502 top       A top-like interface for core services and device control servers.                                                                                 \u2502\n\u2502 core      handle core services: start, stop, status                                                                                                          \u2502\n\u2502 show      Show information about settings, environment, setup, ...                                                                                           \u2502\n\u2502 check     Check installation, settings, required files, etc.                                                                                                 \u2502\n\u2502 dev-x     device-x is an imaginary device that serves as an example                                                                                          \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n

The cgse command is actually an app that is the starting point for a number of commands that can be used to maintain the system, manage and inspect services and devices. For example, to check the version of the different components, use:

$ cgse version\nCGSE-COMMON installed version = 0.5.0 \u2014 Software framework to support hardware testing\nCGSE-CORE installed version = 0.5.0 \u2014 Core services for the CGSE framework\nCGSE-TOOLS installed version = 0.5.0 \u2014 Tools for CGSE\n

We will for now concentrate on the init command. This command will guide us through a number of steps to define the location of our device data, configuration data, etc. We will basically define some environment variables that are used by the CGSE framework. The PROJECT is he name of the project your will be working on, the SITE_ID is the identifier for the LAB or Setup that you are using to perform the tests. As you see below, the environment variables all start with the project name allowing you to work on different projects simultaneously. If you accept all the defaults, the result of the cgse init command will look something like this:

$ cgse init --project marvel\nPlease note default values are given between [brackets].\nWhat is the name of the project [MARVEL] ?:\nWhat is the site identifier ?: lab02\nWhere can the project data be stored [~/data/MARVEL/LAB02/] ?:\nWhere will the configuration data be located [~/data/MARVEL/LAB02/conf/] ?:\nWhere will the logging messages be stored [~/data/MARVEL/LAB02/log/] ?:\nWhere shall I create a local settings YAML file [~/data/MARVEL/LAB02/local_settings.yaml] ?:\nShall I add the environment to your ~/bash_profile ? [y/n]: n\n\n# -> Add the following lines to your bash profile or equivalent\n\nexport PROJECT=MARVEL\nexport SITE_ID=LAB02\nexport MARVEL_DATA_STORAGE_LOCATION=~/data/MARVEL/LAB02/\nexport MARVEL_CONF_DATA_LOCATION=~/data/MARVEL/LAB02/conf/\nexport MARVEL_LOG_FILE_LOCATION=~/data/MARVEL/LAB02/log/\nexport MARVEL_LOCAL_SETTINGS=~/data/MARVEL/LAB02/local_settings.yaml\n

If you answered 'Y' to the last question, you should log in to the shell again with exec bash -login or a similar command for other shells, or you should start a new terminal to activate the environment variables.

Add this point you are ready to go and start the core services and any device control servers that you need. You can explore other commands of the cgse app in the user guide.

"},{"location":"package_list/","title":"Packages in the CGSE","text":"

The CGSE is a monorepo and consists of numerous packages. Each of these packages are individually installable from PyPI. We maintain a list here with all the packages in the monorepo.

Package Description cgse-common The common code used by all other packages cgse-core The core services cgse-coordinates Coordinate reference Frames cgse-gui GUI components and styles (PyQt5) cgse-tools Plugin that adds functions to the cgse command symetrie-hexapod Device drivers for the Sym\u00e9trie Hexapods keithley-tempcontrol Device driver for the Keithley temperature controller plato-fits FITS driver with PLATO specific format plato-hdf5 HDF5 driver with PLATO specific format plato-spw SpaceWire driver with PATO specific packets

The following is a non-exhaustive list of known external packages that work well with the CGSE in terms of commanding and monitoring.

Package Description cgse-dummy Provides a dummy device driver to demonstrate plugins, commands and how to develop an external package for the CGSE."},{"location":"roadmap/","title":"Roadmap","text":"

Don't worry, the feature set will grow..

"},{"location":"roadmap/#features","title":"Features","text":""},{"location":"roadmap/#the-cgse-command","title":"The cgse Command","text":"

Provide a cgse command that is extensible with new commands and command groups:

"},{"location":"roadmap/#settings-setup-and-the-environment","title":"Settings, Setup and the environment","text":""},{"location":"roadmap/#common-functionality","title":"Common functionality","text":""},{"location":"roadmap/#devices","title":"Devices","text":""},{"location":"roadmap/#projects","title":"Projects","text":""},{"location":"roadmap/#guis-and-tuis","title":"GUIs and TUIs","text":""},{"location":"roadmap/#removals","title":"Removals","text":""},{"location":"roadmap/#testing","title":"Testing","text":""},{"location":"tutorial/","title":"Tutorial","text":"

Welcome to the CGSE Tutorial!

By the end of this page you should have a solid understanding of the core features of the CGSE.

"},{"location":"api/","title":"API","text":"

This is a API-level reference to the CGSE API. Click the links to your left (or in the menu) to open a reference for each module.

If you are new to the CGSE, you may want to read the Gettings Started first.

"},{"location":"api/bits/","title":"egse.bits","text":"

This module contains a number of convenience functions to work with bits, bytes and integers.

Functions:

Name Description beautify_binary

Returns a binary representation of the given value. The bits are presented

bit_set

Return True if the bit is set.

bits_set

Return True if all the bits are set.

clear_bit

Set bit to 0 for the given value.

clear_bits

Set the given bits in value to 0.

crc_calc

Calculate the checksum for (part of) the data.

extract_bits

Extracts a specified number of bits from an integer starting at a given position.

humanize_bytes

Represents the size n in human readable form, i.e. as byte, KiB, MiB, GiB, ...

s16

Return the signed equivalent of a hex or binary number.

s32

Return the signed equivalent of a hex or binary number.

set_bit

Set bit to 1 for the given value.

set_bits

Set the given bits in value to 1.

toggle_bit

Toggle the bit in the given value.

"},{"location":"api/bits/#egse.bits.beautify_binary","title":"beautify_binary","text":"
beautify_binary(value, sep=' ', group=8, prefix='', size=0)\n

Returns a binary representation of the given value. The bits are presented in groups of 8 bits for clarity by default (can be changed with the group keyword).

Parameters:

Name Type Description Default value int

the value to beautify

required sep str

the separator character to be used, default is a space

' ' group int

the number of bits to group together, default is 8

8 prefix str

a string to prefix the result, default is ''

'' size int

number of digits

0

Returns:

Type Description str

a binary string representation.

Examples:

>>> status = 2**14 + 2**7\n>>> assert beautify_binary(status) == \"01000000 10000000\"\n
"},{"location":"api/bits/#egse.bits.bit_set","title":"bit_set","text":"
bit_set(value, bit)\n

Return True if the bit is set.

Parameters:

Name Type Description Default value int

the value to check

required bit int

the index of the bit to check, starting from 0 at the LSB

required

Returns:

Type Description bool

True if the bit is set (1).

"},{"location":"api/bits/#egse.bits.bits_set","title":"bits_set","text":"
bits_set(value, *args)\n

Return True if all the bits are set.

Parameters:

Name Type Description Default value int

the value to check

required args

a set of indices of the bits to check, starting from 0 at the LSB

()

Returns:

Type Description bool

True if all the bits are set (1).

Examples:

>>> assert bits_set(0b0101_0000_1011, [0, 1, 3, 8, 10])\n>>> assert bits_set(0b0101_0000_1011, [3, 8])\n>>> assert not bits_set(0b0101_0000_1011, [1, 2, 3])\n
"},{"location":"api/bits/#egse.bits.clear_bit","title":"clear_bit","text":"
clear_bit(value, bit)\n

Set bit to 0 for the given value.

Parameters:

Name Type Description Default value int

the integer value that needs a bit set or unset

required bit int

the index of the bit to set/unset, starting from 0 at the LSB

required

Returns:

Type Description int

the changed value.

"},{"location":"api/bits/#egse.bits.clear_bits","title":"clear_bits","text":"
clear_bits(value, bits)\n

Set the given bits in value to 0. Args: value (int): the value where the given bits shall be changed bits (tuple): a tuple with start and stop bits Returns: the changed value

"},{"location":"api/bits/#egse.bits.crc_calc","title":"crc_calc","text":"
crc_calc(data, start, len)\n

Calculate the checksum for (part of) the data.

Reference

The description of the CRC calculation for RMAP is given in the ECSS document Space Engineering: SpaceWire - Remote Memory Access Protocol, section A.3 on page 80 [ECSS\u2010E\u2010ST\u201050\u201052C].

Parameters:

Name Type Description Default data

the data for which the checksum needs to be calculated

required start int

offset into the data array [byte]

required len int

number of bytes to incorporate into the calculation

required

Returns:

Type Description int

the calculated checksum.

"},{"location":"api/bits/#egse.bits.extract_bits","title":"extract_bits","text":"
extract_bits(value, start_position, num_bits)\n

Extracts a specified number of bits from an integer starting at a given position.

Parameters:

Name Type Description Default value int

The input integer.

required start_position int

The starting bit position (0-based index).

required num_bits int

The number of bits to extract.

required

Returns:

Name Type Description int int

The extracted bits as an integer.

"},{"location":"api/bits/#egse.bits.humanize_bytes","title":"humanize_bytes","text":"
humanize_bytes(n, base=2, precision=3)\n

Represents the size n in human readable form, i.e. as byte, KiB, MiB, GiB, ...

Parameters:

Name Type Description Default n int

number of byte

required base (int, str)

binary (2) or decimal (10)

2 precision int

the number of decimal places [default=3]

3

Returns:

Type Description str

a human readable size, like 512 byte or 2.300 TiB

Raises:

Type Description ValueError

when base is different from 2 (binary) or 10 (decimal).

Examples:

>>> assert humanize_bytes(55) == \"55 bytes\"\n>>> assert humanize_bytes(1024) == \"1.000 KiB\"\n>>> assert humanize_bytes(1000, base=10) == \"1.000 kB\"\n>>> assert humanize_bytes(1000000000) == '953.674 MiB'\n>>> assert humanize_bytes(1000000000, base=10) == '1.000 GB'\n>>> assert humanize_bytes(1073741824) == '1.000 GiB'\n>>> assert humanize_bytes(1024**5 - 1, precision=0) == '1024 TiB'\n
Note

Please note that, by default, I use the IEC standard (International Engineering Consortium) which is in base=2 (binary), i.e. 1024 bytes = 1.0 KiB. If you need SI units (International System of Units), you need to specify base=10 (decimal), i.e. 1000 bytes = 1.0 kB.

"},{"location":"api/bits/#egse.bits.s16","title":"s16","text":"
s16(value)\n

Return the signed equivalent of a hex or binary number.

Returns:

Type Description int

The negative equivalent of a twos-complement binary number.

Examples:

Since integers in Python are objects and stored in a variable number of bits, Python doesn't know the concept of twos-complement for negative integers. For example, this 16-bit number

>>> 0b1000_0000_0001_0001\n32785\n

which in twos-complement is actually a negative value:

>>> s16(0b1000_0000_0001_0001)\n-32751\n

The 'bin()' fuction will return a strange representation of this number:

>>> bin(-32751)\n'-0b111111111101111'\n

when we however mask the value we get:

>>> bin(-32751 & 0b1111_1111_1111_1111)\n'0b1000000000010001'\n
See

Twos complement in Python and Pythons representation of negative integers and Signed equivalent of a twos-complement hex-value and SO Twos complement in Python.

"},{"location":"api/bits/#egse.bits.s32","title":"s32","text":"
s32(value)\n

Return the signed equivalent of a hex or binary number.

Returns:

Type Description

The negative equivalent of a twos-complement binary number.

Examples:

Since integers in Python are objects and stored in a variable number of bits, Python doesn't know the concept of twos-complement for negative integers. For example, this 32-bit number

>>> 0b1000_0000_0000_0000_0000_0000_0001_0001\n2147483665\n

which in twos-complement is actually a negative value:

>>> s32(0b1000_0000_0000_0000_0000_0000_0001_0001)\n-2147483631\n
"},{"location":"api/bits/#egse.bits.set_bit","title":"set_bit","text":"
set_bit(value, bit)\n

Set bit to 1 for the given value.

Parameters:

Name Type Description Default value int

the integer value that needs a bit set or unset

required bit int

the index of the bit to set/unset, starting from 0 at the LSB

required

Returns:

Type Description int

the changed value.

"},{"location":"api/bits/#egse.bits.set_bits","title":"set_bits","text":"
set_bits(value, bits)\n

Set the given bits in value to 1. Args: value (int): the value where the given bits shall be changed bits (tuple): a tuple with start and stop bits Returns: the changed value

"},{"location":"api/bits/#egse.bits.toggle_bit","title":"toggle_bit","text":"
toggle_bit(value, bit)\n

Toggle the bit in the given value.

Parameters:

Name Type Description Default value int

the integer value that needs a bit toggled

required bit int

the index of the bit to toggle, starting from 0 at the LSB

required

Returns:

Type Description int

the changed value.

"},{"location":"api/calibration/","title":"egse.calibration","text":"

This module provides functions to calibrate sensor values.

Functions:

Name Description apply_gain_offset

Applies the given gain and offset to the given counts.

callendar_van_dusen

Solves the Callendar - van Dusen equation for temperature.

chebychev

Solves the Chebychev equation for temperature.

counts_to_resistance

Converts the given counts for the given sensor to resistance.

counts_to_temperature

Converts the given counts for the given sensor to temperature.

resistance_to_temperature

Converts the given resistance for the given sensor to temperature.

solve_temperature

Solves the temperature from the temperature -> resistance polynomial.

"},{"location":"api/calibration/#egse.calibration.apply_gain_offset","title":"apply_gain_offset","text":"
apply_gain_offset(counts, gain, offset)\n

Applies the given gain and offset to the given counts.

Parameters:

Name Type Description Default counts float

Uncalibrated, raw data [ADU]

required gain float

Gain to apply

required offset float

Offset to apply

required

Returns:

Type Description float

Counts after applying the given gain and offset.

"},{"location":"api/calibration/#egse.calibration.callendar_van_dusen","title":"callendar_van_dusen","text":"
callendar_van_dusen(\n    resistance, ref_resistance, standard, setup\n)\n

Solves the Callendar - van Dusen equation for temperature.

Parameters:

Name Type Description Default resistance float

Resistance [Ohm] for which to calculate the temperature

required ref_resistance float

Resistance [Ohm] for a temperature of 0\u00b0C

required standard str

Sensor standard

required setup Setup

Setup

required

Returns:

Type Description float

Temperature [\u00b0C] corresponding to the given resistance.

"},{"location":"api/calibration/#egse.calibration.chebychev","title":"chebychev","text":"
chebychev(resistance, sensor_info)\n

Solves the Chebychev equation for temperature.

Implemented as specified in the calibration certificate of the LakeShore Cernox sensors.

Parameters:

Name Type Description Default resistance float

Resistance [Ohm] for which to calculate the temperature

required sensor_info navdict

Calibration information

required

Returns:

Type Description float

Temperature [\u00b0C] corresponding to the given resistance.

"},{"location":"api/calibration/#egse.calibration.counts_to_resistance","title":"counts_to_resistance","text":"
counts_to_resistance(sensor_name, counts, sensor_info)\n

Converts the given counts for the given sensor to resistance.

Parameters:

Name Type Description Default sensor_name str

Sensor name

required counts float

Uncalibrated, raw data [ADU]

required sensor_info NavigableDict

Calibration information for the given sensor (type)

required

Returns:

Type Description float

Resistance [Ohm] for the given sensor.

"},{"location":"api/calibration/#egse.calibration.counts_to_temperature","title":"counts_to_temperature","text":"
counts_to_temperature(\n    sensor_name, counts, sensor_info, setup\n)\n

Converts the given counts for the given sensor to temperature.

This conversion can be done as follows:

- (1) Directly from counts to temperature, by applying the gain and offset;\n- (2) Directly from counts to temperature, by applying a function;\n- (3) From counts, via resistance, to temperature.\n

Parameters:

Name Type Description Default sensor_name str

Sensor name

required counts float

Uncalibrated, raw data [ADU]

required sensor_info NavigableDict

Calibration information for the given sensor (type)

required setup Setup

Setup

required

Returns:

Type Description float

Calibrated temperature [\u00b0C] for the given sensor

"},{"location":"api/calibration/#egse.calibration.resistance_to_temperature","title":"resistance_to_temperature","text":"
resistance_to_temperature(\n    sensor_name, resistance, sensor_info, setup\n)\n

Converts the given resistance for the given sensor to temperature.

Parameters:

Name Type Description Default sensor_name str

Sensor name

required resistance float

Resistance [Ohm]

required sensor_info NavigableDict

Calibration information for the given sensor (type)

required setup Setup

Setup

required

Returns:

Type Description floaf

Temperature [\u00b0C] for the given sensor.

"},{"location":"api/calibration/#egse.calibration.solve_temperature","title":"solve_temperature","text":"
solve_temperature(\n    temperature_to_resistance_coefficients, resistance\n)\n

Solves the temperature from the temperature -> resistance polynomial.

For the given temperature -> resistance polynomial and the given resistance, we determine what the corresponding temperature is by:

"},{"location":"api/command/","title":"egse.command","text":"

This module defines a number of classes and helper functions to define and work with commands that operate hardware devices. The goal is to be able to define / create commands transparently from a YAML file without having to write (too much) code.

"},{"location":"api/command/#egse.command--definitions","title":"Definitions","text":"

command

a string that is sent to a device over an interface like TCP/IP or USB. This string is generated by the get_cmd_string() method of the Command class.

The string contains format like syntax that looks like an f-string, but is interpreted differently. See further: How to format device command strings.

Command

the base class for commands. This class contains the definition of the command and provides methods to parse and check arguments. The Command can be 'called' or 'executed' in which case a number of actions are performed based on the provided arguments.

CommandExecution

this class contains all the information needed to execute a command, without actually executing it. A CommandExecution contains the command definition and the parameters for the execution. It is mainly served as a communication mechanism to the control servers, i.e. the client side (Proxy) defines a command execution and the server then executes the command.

CommandError

a catch-all exception for unrecoverable errors in this module

InvalidArgumentsError

a CommandError raised when the arguments provided are themselve invalid or if the number of arguments is not matching expectations

The basic interface is:

cmd = Command(name     = <command name>,\n              cmd      = <command string>,\n              response = <callable to retreive a response>,\n              wait     = <callable to wait a specific time/delay>)\n

where:

"},{"location":"api/command/#egse.command--formatting-device-command-strings","title":"Formatting device command strings","text":"

The cmd argument is a string that contains placeholders (replacement fields) for future arguments that will be passed when calling the Command. The replacement fields are marked with curly braces and are mandatory. When a name is provided in the curly braces, the argument shall be provided as a keyword argument, otherwise a positional argument is expected. In the current implementation the cmd can only contain either positional arguments or keyword argument, not a mix of both.

The replacement fields may also have a format specifier to specify a precise format for that field.

Examples

moveAbsolute = Command(\n    name = \"moveAbsolute\",\n    cmd  = \"&2 Q70=0 Q71={tx:.6f} Q72={ty:.6f} Q73={tz:.6f} \"\n           \"Q74={rx:.6f} Q75={ry:.6f} Q76={rz:.6f} Q20=11\"\n)\n\nresponse = moveAbsolute(1, 1, 1, 0, 0, 20)\nresponse = moveAbsolute(tx=1, ty=1, tz=1, rx=0, ry=0, rz=20)\n
"},{"location":"api/command/#egse.command--questions","title":"Questions","text":"

Do we need additional hooks into this commanding?

Classes:

Name Description ClientServerCommand Command

A Command is basically a string that is send to a device and for which the

CommandError

A Command Exception as a base class for this module.

CommandExecution

This class contains all the information that is needed to execute a command

InvalidArgumentsError

The arguments provided are invalid

InvalidCommandExecution

A invalid command execution.

Functions:

Name Description dry_run

This decorator prepares the function to handle a dry run.

get_function

Returns a function (unbound method) from a given class.

get_method

Returns a bound method from a given class instance.

load_commands

Loads the command definitions from the given command_settings and builds an internal

parse_format_string

Parse and decode the format string.

"},{"location":"api/command/#egse.command.ClientServerCommand","title":"ClientServerCommand","text":"
ClientServerCommand(\n    name,\n    cmd,\n    response=None,\n    wait=None,\n    check=None,\n    description=None,\n    device_method=None,\n)\n

Bases: Command

Methods:

Name Description client_call

This method is called at the client side. It is used by the Proxy

server_call

This method is called at the server side. It is used by the CommandProtocol class in the

"},{"location":"api/command/#egse.command.ClientServerCommand.client_call","title":"client_call","text":"
client_call(other, *args, **kwargs)\n

This method is called at the client side. It is used by the Proxy as a generic command to send a command execution to the server.

Parameters:

Name Type Description Default other

a sub-class of the Proxy class

required args

arguments that will be passed on to this command when executed

() kwargs

keyword arguments that will be passed on to this command when executed

{}

Returns:

Type Description

the response that is returned by calling the command (at the server side).

"},{"location":"api/command/#egse.command.ClientServerCommand.server_call","title":"server_call","text":"
server_call(other, *args, **kwargs)\n

This method is called at the server side. It is used by the CommandProtocol class in the execute method.

Parameters:

Name Type Description Default other

a sub-class of the CommandProtocol

required args

arguments are passed on to the response method

() kwargs

keyword arguments are passed on to the response method

{}

Returns:

Type Description

0 on success and -1 on failure.

"},{"location":"api/command/#egse.command.Command","title":"Command","text":"
Command(\n    name,\n    cmd,\n    response=None,\n    wait=None,\n    check=None,\n    description=None,\n    device_method=None,\n)\n

A Command is basically a string that is send to a device and for which the device returns a response.

The command string can contain placeholders that will be filled when the command is 'called'.

The arguments that are given will be filled into the formatted string. Arguments can be positional or keyword arguments, not both.

"},{"location":"api/command/#egse.command.CommandError","title":"CommandError","text":"

Bases: Error

A Command Exception as a base class for this module.

"},{"location":"api/command/#egse.command.CommandExecution","title":"CommandExecution","text":"
CommandExecution(cmd, *args, **kwargs)\n

This class contains all the information that is needed to execute a command with a set of parameters/arguments. The command is however not executed automatically. That is the responsibility of the caller to actually execute the command with the given parameters.

Developer info

you can see this as a partial (functools) which defines the command and its arguments, but doesn't execute until explicitly called. You can execute the command by calling the cmd with the given arguments:

ce = CommandExecution(cmd, 20.0)\n...\nresponse = ce.run()\n
"},{"location":"api/command/#egse.command.InvalidArgumentsError","title":"InvalidArgumentsError","text":"

Bases: CommandError

The arguments provided are invalid

"},{"location":"api/command/#egse.command.InvalidCommandExecution","title":"InvalidCommandExecution","text":"
InvalidCommandExecution(exc, cmd, *args, **kwargs)\n

Bases: CommandExecution

A invalid command execution.

Parameters:

Name Type Description Default exc

the Exception that was raised and describes the problem

required cmd

the Command object

required *args

the positional arguments that were given

() **kwargs

the keyword arguments that were given

{}"},{"location":"api/command/#egse.command.dry_run","title":"dry_run","text":"
dry_run(func)\n

This decorator prepares the function to handle a dry run.

A dry run is used to check the logic of an instrument commanding script without actually executing the instrument commands. The commands are instead added to the command sequence in the global state.

Parameters:

Name Type Description Default func Callable

the function that needs to be executed

required

Returns:

Type Description Callable

A wrapper around the given function.

"},{"location":"api/command/#egse.command.get_function","title":"get_function","text":"
get_function(parent_class, method_name)\n

Returns a function (unbound method) from a given class.

Parameters:

Name Type Description Default parent_class

the class that provides the method

required method_name str

name of the method that is requested

required

Returns:

Type Description Callable

the method [type: function].

Note

The function returned is an unbound class instance method and therefore this function expects as its first argument the class instance, i.e. self, when you call it as a function.

"},{"location":"api/command/#egse.command.get_method","title":"get_method","text":"
get_method(parent_obj, method_name)\n

Returns a bound method from a given class instance.

Parameters:

Name Type Description Default parent_obj

the class instance that provides the method

required method_name str

name of the method that is requested

required

Returns:

Type Description Callable

the method [type: method].

Note

The method returned is an bound class instance method and therefore this method does not expects as its first argument the class instance, i.e. self, when you call this as a function.

"},{"location":"api/command/#egse.command.load_commands","title":"load_commands","text":"
load_commands(\n    protocol_class,\n    command_settings,\n    command_class,\n    device_class,\n)\n

Loads the command definitions from the given command_settings and builds an internal dictionary containing the command names as keys and the corresponding Command class objects as values.

The command_settings is usually loaded from a YAML configuration file containing the command definitions for the device.

Parameters:

Name Type Description Default protocol_class

the CommandProtocol or a sub-class

required command_settings

a dictionary containing the command definitions for this device

required command_class

the type of command to create, a subclass of Command

required device_class

the type of the base device class from which the methods are loaded

required"},{"location":"api/command/#egse.command.parse_format_string","title":"parse_format_string","text":"
parse_format_string(fstring)\n

Parse and decode the format string.

"},{"location":"api/config/","title":"egse.config","text":"

This module provides convenience functions to properly configure the CGSE and to find paths and resources.

Classes:

Name Description WorkingDirectory

WorkingDirectory is a context manager to temporarily change the working directory while

Functions:

Name Description find_dir

Find the first folder that matches the given pattern.

find_dirs

Generator for returning directory paths from a walk started at root and matching pattern.

find_file

Find the path to the given file starting from the root directory of the

find_files

Generator for returning file paths from a top folder, matching the pattern.

find_first_occurrence_of_dir

Returns the full path of the directory that first matches the pattern. The directory hierarchy is

find_root

Find the root folder based on the files in tests.

get_common_egse_root

Returns the absolute path to the installation directory for the Common-EGSE.

get_resource_dirs

Define directories that contain resources like images, icons, and data files.

get_resource_path

Searches for a data file (resource) with the given name.

set_logger_levels

Set the logging level for the given loggers.

"},{"location":"api/config/#egse.config.WorkingDirectory","title":"WorkingDirectory","text":"
WorkingDirectory(path)\n

WorkingDirectory is a context manager to temporarily change the working directory while executing some code.

This context manager has a property path which returns the absolute path of the current directory.

Parameters:

Name Type Description Default path (str, Path)

the folder to change to within this context

required

Raises:

Type Description ValueError

when the given path doesn't exist.

Example
with WorkingDirectory(find_dir(\"/egse/images\")) as wdir:\n    for file in wdir.path.glob('*'):\n        assert file.exists()  # do something with the image files\n

Attributes:

Name Type Description path

Resolve and return the current Path of the context.

"},{"location":"api/config/#egse.config.WorkingDirectory.path","title":"path property","text":"
path\n

Resolve and return the current Path of the context.

"},{"location":"api/config/#egse.config.find_dir","title":"find_dir","text":"
find_dir(pattern, root=None)\n

Find the first folder that matches the given pattern.

Note that if there are more folders that match the pattern in the distribution, this function only returns the first occurrence that is found, which might not be what you want. To be sure only one folder is returned, use the find_dirs() function and check if there is just one item returned in the list.

Parameters:

Name Type Description Default pattern str

pattern to match (use * for wildcard)

required root str

the top level folder to search [default=common-egse-root]

None

Returns:

Type Description Path | None

the first occurrence of the directory pattern or None when not found.

"},{"location":"api/config/#egse.config.find_dirs","title":"find_dirs","text":"
find_dirs(pattern, root=None)\n

Generator for returning directory paths from a walk started at root and matching pattern.

The pattern can contain the asterisk '*' as a wildcard.

The pattern can contain a directory separator '/' which means the last part of the path needs to match these folders.

Parameters:

Name Type Description Default pattern str

pattern to match (use * for wildcard)

required root str

the top level folder to search [default=common-egse-root]

None

Returns:

Name Type Description Generator None

Paths of folders matching pattern, from root.

Example
>>> for folder in find_dirs(\"/egse/images\"):\n...     assert folder.match('*/egse/images')\n\n>>> folders = list(find_dirs(\"/egse/images\"))\n>>> assert len(folders)\n
"},{"location":"api/config/#egse.config.find_file","title":"find_file","text":"
find_file(name, root=None, in_dir=None)\n

Find the path to the given file starting from the root directory of the distribution.

Note that if there are more files with the given name found in the distribution, this function only returns the first file that is found, which might not be what you want. To be sure only one file is returned, use the find_files() function and check if there is just one file returned in the list.

When the file shall be in a specific directory, use the in_dir keyword. This requires that the path ends with the given string in in_dir.

>>> file_pattern = 'EtherSpaceLink*.dylib'\n>>> in_dir = 'lib/CentOS-7'\n>>> file = find_file(file_pattern, in_dir=in_dir)\n>>> assert file.match(\"*/lib/CentOS-7/EtherSpace*\")\n

Parameters:

Name Type Description Default name str

the name of the file

required root str

the top level folder to search [default=common-egse-root]

None in_dir str

the 'leaf' directory in which the file shall be

None

Returns:

Type Description Path | None

the first occurrence of the file or None when not found.

"},{"location":"api/config/#egse.config.find_files","title":"find_files","text":"
find_files(pattern, root=None, in_dir=None)\n

Generator for returning file paths from a top folder, matching the pattern.

The top folder can be specified as e.g. __file__ in which case the parent of that file will be used as the top root folder. Note that when you specify '.' as the root argument the current working directory will be taken as the root folder, which is probably not what you intended.

When the file shall be in a specific directory, use the in_dir keyword. This requires that the path ends with the given string in in_dir.

>>> file_pattern = 'EtherSpaceLink*.dylib'\n>>> in_dir = 'lib/CentOS-7'\n>>> for file in find_files(file_pattern, in_dir=in_dir):\n...     assert file.match(\"*lib/CentOS-7/EtherSpaceLink*\")\n

Parameters:

Name Type Description Default pattern str)

sorting pattern (use * for wildcard)

required root str

the top level folder to search [default=common-egse-root]

None in_dir str

the 'leaf' directory in which the file shall be

None

Returns:

Type Description

Paths of files matching pattern, from root.

"},{"location":"api/config/#egse.config.find_first_occurrence_of_dir","title":"find_first_occurrence_of_dir","text":"
find_first_occurrence_of_dir(pattern, root=None)\n

Returns the full path of the directory that first matches the pattern. The directory hierarchy is traversed in alphabetical order. The pattern is matched first against all directories in the root folder, if there is no match, the first folder in root is traversed until a match is found. If no match is found, the second folder in root is traversed.

Note that the pattern may contain parent directories, like /egse/data/icons or egse/*/icons, in which case the full pattern is matched.

Parameters:

Name Type Description Default pattern str

a filename pattern

required root Path | str

the root folder to start the hierarchical search

None

Returns:

Type Description Path | None

The full path of the matched pattern or None if no match could be found.

"},{"location":"api/config/#egse.config.find_root","title":"find_root","text":"
find_root(path, tests=(), default=None)\n

Find the root folder based on the files in tests.

The algorithm crawls backward over the directory structure until one of the items in tests is matched. and it will return that directory as a Path.

When no root folder can be determined, the default parameter is returned as a Path (or None).

When nothing is provided in tests, all matches will fail and the default parameter will be returned.

Parameters:

Name Type Description Default path Union[str, PurePath] | None

folder from which the search is started

required tests Tuple[str, ...]

names (files or dirs) to test for existence

() default str

returned when no root is found

None

Returns:

Type Description Union[PurePath, None]

a Path which is the root folder.

"},{"location":"api/config/#egse.config.get_common_egse_root","title":"get_common_egse_root cached","text":"
get_common_egse_root(path=None)\n

Returns the absolute path to the installation directory for the Common-EGSE.

The algorithm first tries to determine the path from the environment variable PLATO_COMMON_EGSE_PATH. If this environment variable doesn't exist, the algorithm tries to determine the path automatically from (1) the git root if it is a git repository, or (2) from the location of this module assuming the installation is done from the GitHub distribution.

When the optional argument path is given, that directory will be used to start the search for the root folder.

At this moment the algorithm does not cache the egse_path in order to speed up the successive calls to this function.

Parameters:

Name Type Description Default path str or Path

a directory as a Path or str [optional]

None

Returns:

Name Type Description Path Optional[PurePath]

the absolute path to the Common-EGSE installation directory or None

"},{"location":"api/config/#egse.config.get_resource_dirs","title":"get_resource_dirs","text":"
get_resource_dirs(root_dir=None)\n

Define directories that contain resources like images, icons, and data files.

Resource directories can have the following names: resources, data, icons, or images. This function checks if any of the resource directories exist in the project root directory, in the root_dir that is given as an argument or in the src/egse sub-folder.

So, the directories that are searched for the resource folders are:

For all existing directories the function returns the absolute path.

Parameters:

Name Type Description Default root_dir str

the directory to search for resource folders

None

Returns:

Type Description List[Path]

a list of absolute Paths.

"},{"location":"api/config/#egse.config.get_resource_path","title":"get_resource_path","text":"
get_resource_path(name, resource_root_dir=None)\n

Searches for a data file (resource) with the given name.

When resource_root_dir is not given, the search for resources will start at the root folder of the project (using the function get_common_egse_root()). Any other root directory can be given, e.g. if you want to start the search from the location of your source code file, use Path(__file__).parent as the resource_root_dir argument.

Parameters:

Name Type Description Default name str

the name of the resource that is requested

required resource_root_dir str

the root directory where the search for resources should be started

None

Returns:

Type Description PurePath

the absolute path of the data file with the given name. The first name that matches is returned. If no file with the given name or path exists, a FileNotFoundError is raised.

"},{"location":"api/config/#egse.config.set_logger_levels","title":"set_logger_levels","text":"
set_logger_levels(logger_levels=None)\n

Set the logging level for the given loggers.

"},{"location":"api/control/","title":"egse.control","text":"

This module defines the abstract class for any Control Server and some convenience functions.

Classes:

Name Description ControlServer

Base class for all device control servers and for the Storage Manager and Configuration Manager.

Functions:

Name Description is_control_server_active

Checks if the Control Server is running.

"},{"location":"api/control/#egse.control.ControlServer","title":"ControlServer","text":"
ControlServer()\n

Base class for all device control servers and for the Storage Manager and Configuration Manager.

A Control Server reads commands from a ZeroMQ socket and executes these commands by calling the execute() method of the commanding protocol class.

The subclass shall define the following:

Methods:

Name Description after_serve

This method needs to be overridden by the subclass if certain actions need to be executed after the control

before_serve

This method needs to be overridden by the subclass if certain actions need to be executed before the control

get_average_execution_times

Returns the average execution times of all functions that have been monitored by this process.

get_commanding_port

Returns the commanding port used by the Control Server.

get_communication_protocol

Returns the communication protocol used by the Control Server.

get_ip_address

Returns the IP address of the current host.

get_monitoring_port

Returns the monitoring port used by the Control Server.

get_process_status

Returns the process status of the Control Server.

get_service_port

Returns the service port used by the Control Server.

get_storage_mnemonic

Returns the storage mnemonics used by the Control Server.

handle_scheduled_tasks

Executes or reschedules tasks in the serve() event loop.

is_storage_manager_active

Checks if the Storage Manager is active.

notify_listeners

Notifies registered listeners about an event.

quit

Interrupts the Control Server.

register_as_listener

Registers a listener with the specified proxy.

register_to_storage_manager

Registers this Control Server to the Storage Manager.

schedule_task

Schedules a task to run in the control server event loop.

serve

Activation of the Control Server.

set_hk_delay

Sets the delay time for housekeeping.

set_logging_level

Sets the logging level to the given level.

set_mon_delay

Sets the delay time for monitoring.

set_scheduled_task_delay

Sets the delay time between successive executions of scheduled tasks.

store_housekeeping_information

Sends housekeeping information to the Storage Manager.

unregister_as_listener

Removes a registered listener from the specified proxy.

unregister_from_storage_manager

Unregisters the Control Server from the Storage Manager.

"},{"location":"api/control/#egse.control.ControlServer.after_serve","title":"after_serve","text":"
after_serve()\n

This method needs to be overridden by the subclass if certain actions need to be executed after the control server has been deactivated.

"},{"location":"api/control/#egse.control.ControlServer.before_serve","title":"before_serve","text":"
before_serve()\n

This method needs to be overridden by the subclass if certain actions need to be executed before the control server is activated.

"},{"location":"api/control/#egse.control.ControlServer.get_average_execution_times","title":"get_average_execution_times","text":"
get_average_execution_times()\n

Returns the average execution times of all functions that have been monitored by this process.

Returns:

Type Description dict

Dictionary with the average execution times of all functions that have been monitored by this process. The dictionary keys are the function names, and the values are the average execution times in ms.

"},{"location":"api/control/#egse.control.ControlServer.get_commanding_port","title":"get_commanding_port abstractmethod","text":"
get_commanding_port()\n

Returns the commanding port used by the Control Server.

Returns:

Type Description int

Commanding port used by the Control Server, as specified in the settings.

"},{"location":"api/control/#egse.control.ControlServer.get_communication_protocol","title":"get_communication_protocol abstractmethod","text":"
get_communication_protocol()\n

Returns the communication protocol used by the Control Server.

Returns:

Type Description str

Communication protocol used by the Control Server, as specified in the settings.

"},{"location":"api/control/#egse.control.ControlServer.get_ip_address","title":"get_ip_address","text":"
get_ip_address()\n

Returns the IP address of the current host.

"},{"location":"api/control/#egse.control.ControlServer.get_monitoring_port","title":"get_monitoring_port abstractmethod","text":"
get_monitoring_port()\n

Returns the monitoring port used by the Control Server.

Returns:

Type Description int

Monitoring port used by the Control Server, as specified in the settings.

"},{"location":"api/control/#egse.control.ControlServer.get_process_status","title":"get_process_status","text":"
get_process_status()\n

Returns the process status of the Control Server.

Returns:

Type Description dict

Dictionary with the process status of the Control Server.

"},{"location":"api/control/#egse.control.ControlServer.get_service_port","title":"get_service_port abstractmethod","text":"
get_service_port()\n

Returns the service port used by the Control Server.

Returns:

Type Description int

Service port used by the Control Server, as specified in the settings.

"},{"location":"api/control/#egse.control.ControlServer.get_storage_mnemonic","title":"get_storage_mnemonic","text":"
get_storage_mnemonic()\n

Returns the storage mnemonics used by the Control Server.

This is a string that will appear in the filename with the housekeeping information of the device, as a way of identifying the device. If this is not implemented in the subclass, then the class name will be used.

Returns:

Type Description str

Storage mnemonics used by the Control Server, as specified in the settings.

"},{"location":"api/control/#egse.control.ControlServer.handle_scheduled_tasks","title":"handle_scheduled_tasks","text":"
handle_scheduled_tasks()\n

Executes or reschedules tasks in the serve() event loop.

"},{"location":"api/control/#egse.control.ControlServer.is_storage_manager_active","title":"is_storage_manager_active","text":"
is_storage_manager_active()\n

Checks if the Storage Manager is active.

This method has to be implemented by the subclass if you need to store information.

Note: You might want to set a specific timeout when checking for the Storage Manager.

Note: If this method returns True, the following methods shall also be implemented by the subclass:

Returns:

Type Description bool

True if the Storage Manager is active; False otherwise.

"},{"location":"api/control/#egse.control.ControlServer.notify_listeners","title":"notify_listeners","text":"
notify_listeners(event_id=0, context=None)\n

Notifies registered listeners about an event.

This function creates an Event object with the provided event_id and context and notifies all registered listeners with the created event.

Parameters:

Name Type Description Default event_id int

The identifier for the event. Defaults to 0.

0 context dict

Additional context information associated with the event. Defaults to None.

None Note

The notification is performed by the notify_listeners method of the listeners object associated with this instance. The notification is executed in a daemon thread to avoid blocking the commanding chain.

"},{"location":"api/control/#egse.control.ControlServer.quit","title":"quit","text":"
quit()\n

Interrupts the Control Server.

"},{"location":"api/control/#egse.control.ControlServer.register_as_listener","title":"register_as_listener","text":"
register_as_listener(proxy, listener)\n

Registers a listener with the specified proxy.

This function attempts to add the provided listener to the specified proxy. It employs a retry mechanism to handle potential ConnectionError exceptions, making up to 5 attempts to add the listener.

Parameters:

Name Type Description Default proxy Type

A callable object representing the proxy to which the listener will be added.

required listener dict

The listener to be registered. Should be a dictionary containing listener details.

required

Raises:

Type Description ConnectionError

If the connection to the proxy encounters issues even after multiple retry attempts.

Note

The function runs in a separate daemon thread to avoid blocking the main thread.

"},{"location":"api/control/#egse.control.ControlServer.register_to_storage_manager","title":"register_to_storage_manager","text":"
register_to_storage_manager()\n

Registers this Control Server to the Storage Manager.

By doing so, the housekeeping information of the device will be sent to the Storage Manager, which will store the information in a dedicated CSV file.

This method has to be overwritten by the subclasses if they have housekeeping information that must be stored.

Subclasses need to overwrite this method if they have housekeeping information to be stored.

The following information is required for the registration:

The egse.storage module provides a convenience method that can be called from the method in the subclass:

>>> from egse.storage import register_to_storage_manager  # noqa\n
Note

the egse.storage module might not be available, it is provided by the cgse-core package.

"},{"location":"api/control/#egse.control.ControlServer.schedule_task","title":"schedule_task","text":"
schedule_task(callback, after=0.0, when=None)\n

Schedules a task to run in the control server event loop.

The callback function will be executed as soon as possible in the serve() event loop.

Some simple scheduling options are available:

The after and the when arguments can be combined.

Note "},{"location":"api/control/#egse.control.ControlServer.serve","title":"serve","text":"
serve()\n

Activation of the Control Server.

This comprises the following steps:

"},{"location":"api/control/#egse.control.ControlServer.set_hk_delay","title":"set_hk_delay","text":"
set_hk_delay(seconds)\n

Sets the delay time for housekeeping.

The delay time is the time between two successive executions of the get_housekeeping() function of the device protocol.

It might happen that the delay time that is set is longer than what you requested. That is the case when the execution of the get_housekeeping() function takes longer than the requested delay time. That should prevent the server from blocking when a too short delay time is requested.

Parameters:

Name Type Description Default seconds float

Number of seconds between the housekeeping calls

required

Returns:

Type Description float

Delay that was set [ms].

"},{"location":"api/control/#egse.control.ControlServer.set_logging_level","title":"set_logging_level","text":"
set_logging_level(level)\n

Sets the logging level to the given level.

Allowed logging levels are:

Parameters:

Name Type Description Default level int | str

Logging level to use, specified as either a string or an integer

required"},{"location":"api/control/#egse.control.ControlServer.set_mon_delay","title":"set_mon_delay","text":"
set_mon_delay(seconds)\n

Sets the delay time for monitoring.

The delay time is the time between two successive executions of the get_status() function of the device protocol.

It might happen that the delay time that is set is longer than what you requested. That is the case when the execution of the get_status() function takes longer than the requested delay time. That should prevent the server from blocking when a too short delay time is requested.

Parameters:

Name Type Description Default seconds float

Number of seconds between the monitoring calls

required

Returns:

Type Description float

Delay that was set [ms].

"},{"location":"api/control/#egse.control.ControlServer.set_scheduled_task_delay","title":"set_scheduled_task_delay","text":"
set_scheduled_task_delay(seconds)\n

Sets the delay time between successive executions of scheduled tasks.

Parameters:

Name Type Description Default seconds

the time interval between two successive executions [seconds]

required"},{"location":"api/control/#egse.control.ControlServer.store_housekeeping_information","title":"store_housekeeping_information","text":"
store_housekeeping_information(data)\n

Sends housekeeping information to the Storage Manager.

This method has to be overwritten by the subclasses if they want the device housekeeping information to be saved.

Parameters:

Name Type Description Default data dict

a dictionary containing parameter name and value of all device housekeeping. There is also a timestamp that represents the date/time when the HK was received from the device.

required"},{"location":"api/control/#egse.control.ControlServer.unregister_as_listener","title":"unregister_as_listener","text":"
unregister_as_listener(proxy, listener)\n

Removes a registered listener from the specified proxy.

This function attempts to remove the provided listener from the specified proxy. It employs a retry mechanism to handle potential ConnectionError exceptions, making up to 5 attempts to add the listener.

Parameters:

Name Type Description Default proxy Type

A callable object representing the proxy from which the listener will be removed.

required listener dict

The listener to be removed. Should be a dictionary containing listener details.

required

Raises:

Type Description ConnectionError

If the connection to the proxy encounters issues even after multiple retry attempts.

Note

The function runs in a separate thread but will block until the de-registration is finished. The reason being that this method is usually called in a after_serve block so it needs to finish before the ZeroMQ context is destroyed.

"},{"location":"api/control/#egse.control.ControlServer.unregister_from_storage_manager","title":"unregister_from_storage_manager","text":"
unregister_from_storage_manager()\n

Unregisters the Control Server from the Storage Manager.

This method has to be overwritten by the subclasses.

The following information is required for the registration:

The egse.storage module provides a convenience method that can be called from the method in the subclass:

>>> from egse.storage import unregister_from_storage_manager  # noqa\n
Note

the egse.storage module might not be available, it is provided by the cgse-core package.

"},{"location":"api/control/#egse.control.is_control_server_active","title":"is_control_server_active","text":"
is_control_server_active(endpoint=None, timeout=0.5)\n

Checks if the Control Server is running.

This function sends a Ping message to the Control Server and expects a Pong answer back within the timeout period.

Parameters:

Name Type Description Default endpoint str

Endpoint to connect to, i.e. ://: None timeout float

Timeout when waiting for a reply [s, default=0.5]

0.5"},{"location":"api/counter/","title":"egse.counter","text":"

This module manages files that have a counter in their filename.

Functions:

Name Description counter_exists

Returns True if the given file exists, False otherwise.

counter_filename

Creates an absolute filename to be used as a counter file. A counter file usually has a 'count' extension

get_next_counter

Read the counter from a dedicated file, add one and save the counter back to the file.

new_counter

Create a counter based on the files that already exist for the given pattern.

"},{"location":"api/counter/#egse.counter.counter_exists","title":"counter_exists","text":"
counter_exists(filename)\n

Returns True if the given file exists, False otherwise.

Parameters:

Name Type Description Default filename Path

path of the counter file

required

Returns:

Type Description bool

True if the given filename exists, False otherwise.

Note

No checking is done if the file is indeed a counter file, i.e. if it contains the correct content. So, this function basically only checks if the given Path exists and if it is a regular file.

"},{"location":"api/counter/#egse.counter.counter_filename","title":"counter_filename","text":"
counter_filename(location, filename)\n

Creates an absolute filename to be used as a counter file. A counter file usually has a 'count' extension but that is not enforced by this module. The location can be a relative path, even '.' or '..' are accepted.

Parameters:

Name Type Description Default location Path

the location of the counter file.

required filename Path | str

the name of the counter file, use the '.count' extension.

required

Returns:

Type Description Path

An absolute filename.

Note

If the file doesn't exist, it is NOT created.

"},{"location":"api/counter/#egse.counter.determine_counter_from_dir_list","title":"determine_counter_from_dir_list","text":"
determine_counter_from_dir_list(\n    location, pattern, index=-1\n)\n

Determine counter for a new file at the given location and with the given pattern. The next counter is determined from the sorted list of files that match the given pattern.

Parameters:

Name Type Description Default location

Location where the file should be stored.

required pattern

Pattern for the filename.

required index int

the location of the counter in the filename after it is split on '_' [default=-1]

-1

Returns:

Type Description int

The value of the next counter, 1 if no previous files were found or if an error occurred.

"},{"location":"api/counter/#egse.counter.get_next_counter","title":"get_next_counter","text":"
get_next_counter(filename)\n

Read the counter from a dedicated file, add one and save the counter back to the file.

Parameters:

Name Type Description Default file_path

full pathname of the file that contains the required counter

required

Returns:

Type Description int

The value of the next counter, 1 if no previous files were found or if an error occurred.

Note

This will create the counter file if it doesn't exist.

"},{"location":"api/counter/#egse.counter.new_counter","title":"new_counter","text":"
new_counter(filename, pattern)\n

Create a counter based on the files that already exist for the given pattern.

Parameters:

Name Type Description Default filename Path

the name of the counter file

required pattern str

a pattern to match the filenames

required

Returns:

Type Description int

The next counter value as an integer.

"},{"location":"api/decorators/","title":"egse.decorators","text":"

A collection of useful decorator functions.

Classes:

Name Description Nothing

Just to get a nice repr for Nothing. It is kind of a Null object...

Profiler

A simple profiler class that provides some useful functions to profile a function.

classproperty

Defines a read-only class property.

Functions:

Name Description average_time

This is a decorator that is intended mainly as a development aid. When you decorate your function with

borg

Use the Borg pattern to make a class with a shared state between its instances and subclasses.

debug

Logs the function signature and return value.

deprecate

Deprecate a function or method. This will print a warning with the function name and where

dynamic_interface

Adds a static variable __dynamic_interface to a method.

profile

Prints the function signature and return value to stdout.

profile_func

A time profiler decorator.

query_command

Adds a static variable __query_command to a method.

read_command

Adds a static variable __read_command to a method.

retry

Decorator that retries a function multiple times with a delay between attempts.

retry_with_exponential_backoff

Decorator for retrying a function with exponential backoff.

singleton

Use class as a singleton.

spy_on_attr_change

Tweak an object to show attributes changing. The changes are reported as WARNING log messages

static_vars

Define static variables in a function.

time_it

Print the runtime of the decorated function.

timer

Print the runtime of the decorated function.

to_be_implemented

Print a warning message that this function/method has to be implemented.

transaction_command

Adds a static variable __transaction_command to a method.

write_command

Adds a static variable __write_command to a method.

"},{"location":"api/decorators/#egse.decorators.Nothing","title":"Nothing","text":"

Just to get a nice repr for Nothing. It is kind of a Null object...

"},{"location":"api/decorators/#egse.decorators.Profiler","title":"Profiler","text":"

A simple profiler class that provides some useful functions to profile a function.

Examples:

>>> from egse.decorators import Profiler\n>>> @Profiler.count()\n... def square(x):\n...     return x**2\n
>>> x = [square(x) for x in range(1_000_000)]\n
>>> print(f\"Function 'square' called {square.get_count()} times.\")\n>>> print(square)\n
>>> @Profiler.duration()\n... def square(x):\n...     time.sleep(0.1)\n...     return x**2\n
>>> x = [square(x) for x in range(100)]\n
>>> print(f\"Function 'square' takes on average {square.get_average_duration():.6f} seconds.\")\n>>> print(square)\n
"},{"location":"api/decorators/#egse.decorators.classproperty","title":"classproperty","text":"
classproperty(func)\n

Defines a read-only class property.

Examples:

>>> class Message:\n...     def __init__(self, msg):\n...         self._msg = msg\n...\n...     @classproperty\n...     def name(cls):\n...         return cls.__name__\n\n>>> msg = Message(\"a simple doctest\")\n>>> assert \"Message\" == msg.name\n
"},{"location":"api/decorators/#egse.decorators.average_time","title":"average_time","text":"
average_time(\n    *, name=\"average_time\", level=INFO, precision=6\n)\n

This is a decorator that is intended mainly as a development aid. When you decorate your function with @average_time, the execution time of your function will be kept and accumulated. At anytime in your code, you can request the total execution time and the number of calls:

@average_time()\ndef my_function():\n    ...\ntotal_execution_time, call_count = my_function.report()\n

Requesting the report will automatically log the average runtime and the number of calls. If you need to reset the execution time and the number of calls during your testing, use:

my_function.reset()\n

Parameters:

Name Type Description Default name str

A name for the timer that will be used during reporting, default='average_time'

'average_time' level int

the required log level, default=logging.INFO

INFO precision int

the precision used to report the average time, default=6

6

Returns:

Type Description

The decorated function.

"},{"location":"api/decorators/#egse.decorators.borg","title":"borg","text":"
borg(cls)\n

Use the Borg pattern to make a class with a shared state between its instances and subclasses.

from

we don't need no singleton

"},{"location":"api/decorators/#egse.decorators.debug","title":"debug","text":"
debug(func)\n

Logs the function signature and return value.

"},{"location":"api/decorators/#egse.decorators.deprecate","title":"deprecate","text":"
deprecate(reason=None, alternative=None)\n

Deprecate a function or method. This will print a warning with the function name and where it is called from. If the optional parameters reason and alternative are given, that information will be printed with the warning.

Examples:

@deprecate(reason=\"it doesn't follow PEP8\", alternative=\"set_color()\")\ndef setColor(self, color):\n    self.set_color(color)\n

Parameters:

Name Type Description Default reason Optional[str]

provide a short explanation why this function is deprecated. Generates 'because {reason}'

None alternative Optional[str]

provides an alternative function/parameters to be used. Generates 'Use {alternative}

None

Returns:

Type Description Callable

The decorated function.

"},{"location":"api/decorators/#egse.decorators.dynamic_interface","title":"dynamic_interface","text":"
dynamic_interface(func)\n

Adds a static variable __dynamic_interface to a method.

The intended use of this function is as a decorator for functions in an interface class.

The static variable is currently used by the Proxy class to check if a method is meant to be overridden dynamically. The idea behind this is to loosen the contract of an abstract base class (ABC) into an interface. For an ABC, the abstract methods must be implemented at construction/initialization. This is not possible for the Proxy subclasses as they load their commands (i.e. methods) from the control server, and the method will be added to the Proxy interface after loading. Nevertheless, we like the interface already defined for auto-completion during development or interactive use.

When a Proxy subclass that implements an interface with methods decorated by the @dynamic_interface does overwrite one or more of the decorated methods statically, these methods will not be dynamically overwritten when loading the interface from the control server. A warning will be logged instead.

"},{"location":"api/decorators/#egse.decorators.profile","title":"profile","text":"
profile(func)\n

Prints the function signature and return value to stdout.

This function checks the Settings.profiling() value and only prints out profiling information if this returns True.

Profiling can be activated with Settings.set_profiling(True).

"},{"location":"api/decorators/#egse.decorators.profile_func","title":"profile_func","text":"
profile_func(\n    output_file=None,\n    sort_by=\"cumulative\",\n    lines_to_print=None,\n    strip_dirs=False,\n)\n

A time profiler decorator.

Parameters:

Name Type Description Default output_file

str or None. Default is None Path of the output file. If only name of the file is given, it's saved in the current directory. If it's None, the name of the decorated function is used.

None sort_by

str or SortKey enum or tuple/list of str/SortKey enum Sorting criteria for the Stats object. For a list of valid string and SortKey refer to: https://docs.python.org/3/library/profile.html#pstats.Stats.sort_stats

'cumulative' lines_to_print

int or None Number of lines to print. Default (None) is for all the lines. This is useful in reducing the size of the printout, especially that sorting by 'cumulative', the time consuming operations are printed toward the top of the file.

None strip_dirs

bool Whether to remove the leading path info from file names. This is also useful in reducing the size of the printout

False

Returns:

Type Description

Profile of the decorated function

Note

This code was taken from this gist: a profile decorator.

Inspired by and modified the profile decorator of Giampaolo Rodola: profile decorato.

"},{"location":"api/decorators/#egse.decorators.query_command","title":"query_command","text":"
query_command(func)\n

Adds a static variable __query_command to a method.

"},{"location":"api/decorators/#egse.decorators.read_command","title":"read_command","text":"
read_command(func)\n

Adds a static variable __read_command to a method.

"},{"location":"api/decorators/#egse.decorators.retry","title":"retry","text":"
retry(times=3, wait=10.0, exceptions=None)\n

Decorator that retries a function multiple times with a delay between attempts.

This decorator can be applied to a function to handle specified exceptions by retrying the function execution. It will make up to 'times' attempts with a waiting period of 'wait' seconds between each attempt. Any exception from the list provided in the exceptions argument will be ignored for the given times.

If after times attempts still an exception is raised, it will be passed through the calling function, otherwise the functions return value will be returned.

Parameters:

Name Type Description Default times int

The number of retry attempts. Defaults to 3.

3 wait float

The waiting period between retries in seconds. Defaults to 10.0.

10.0 exceptions List[Exception] or None

List of exception types to catch and retry. Defaults to None, which catches all exceptions.

None

Returns:

Name Type Description Callable

The decorated function.

Example

Apply the retry decorator to a function with specific retry settings:

@retry(times=5, wait=15.0, exceptions=[ConnectionError, TimeoutError])\ndef my_function():\n    # Function logic here\n
Note

The decorator catches specified exceptions and retries the function, logging information about each retry attempt.

"},{"location":"api/decorators/#egse.decorators.retry_with_exponential_backoff","title":"retry_with_exponential_backoff","text":"
retry_with_exponential_backoff(\n    max_attempts=5,\n    initial_wait=1.0,\n    backoff_factor=2,\n    exceptions=None,\n)\n

Decorator for retrying a function with exponential backoff.

This decorator can be applied to a function to handle specified exceptions by retrying the function execution. It will make up to 'max_attempts' attempts with a waiting period that grows exponentially between each attempt (dependent on the backoff_factor). Any exception from the list provided in the exceptions argument will be ignored for the given max_attempts.

If after all attempts still an exception is raised, it will be passed through the calling function, otherwise the functions return value will be returned.

Parameters:

Name Type Description Default max_attempts

The maximum number of attempts to make.

5 initial_wait

The initial waiting time in seconds before retrying after the first failure.

1.0 backoff_factor

The factor by which the wait time increases after each failure.

2

Returns:

Type Description

The response from the executed function.

"},{"location":"api/decorators/#egse.decorators.singleton","title":"singleton","text":"
singleton(cls)\n

Use class as a singleton.

from

Decorator library: Signleton

"},{"location":"api/decorators/#egse.decorators.spy_on_attr_change","title":"spy_on_attr_change","text":"
spy_on_attr_change(obj, obj_name=None)\n

Tweak an object to show attributes changing. The changes are reported as WARNING log messages in the egse.spy logger.

Note this is not a decorator, but a function that changes the class of an object.

Note that this function is a debugging aid and should not be used in production code!

Parameters:

Name Type Description Default obj object

any object that you want to monitor

required obj_name str

the variable name of the object that was given in the code, if None than the class name will be printed.

None Example
class X:\n   pass\n\nx = X()\nspy_on_attr_change(x, obj_name=\"x\")\nx.a = 5\n
From

Adding a dunder to an object

"},{"location":"api/decorators/#egse.decorators.static_vars","title":"static_vars","text":"
static_vars(**kwargs)\n

Define static variables in a function.

The static variable can be accessed with . inside the function body. Example

@static_vars(count=0)\ndef special_count():\n    return special_count.count += 2\n
"},{"location":"api/decorators/#egse.decorators.time_it","title":"time_it","text":"
time_it(count=1000, precision=4)\n

Print the runtime of the decorated function.

This is a simple replacement for the builtin timeit function. The purpose is to simplify calling a function with some parameters.

The intended way to call this is as a function:

value = function(args)\n\nvalue = time_it(10_000)(function)(args)\n

The time_it function can be called as a decorator in which case it will always call the function count times which is probably not what you want.

Parameters:

Name Type Description Default count int

the number of executions [default=1000].

1000 precision int

the number of significant digits [default=4]

4

Returns:

Name Type Description value

the return value of the last function execution.

See also

the Timer context manager located in egse.system.

Usage
@time_it(count=10000)\ndef function(args):\n    pass\n\ntime_it(10000)(function)(args)\n
"},{"location":"api/decorators/#egse.decorators.timer","title":"timer","text":"
timer(*, name='timer', level=INFO, precision=4)\n

Print the runtime of the decorated function.

Parameters:

Name Type Description Default name str

a name for the Timer, will be printed in the logging message

'timer' level int

the logging level for the time message [default=INFO]

INFO precision int

the number of decimals for the time [default=3 (ms)]

4"},{"location":"api/decorators/#egse.decorators.to_be_implemented","title":"to_be_implemented","text":"
to_be_implemented(func)\n

Print a warning message that this function/method has to be implemented.

"},{"location":"api/decorators/#egse.decorators.transaction_command","title":"transaction_command","text":"
transaction_command(func)\n

Adds a static variable __transaction_command to a method.

"},{"location":"api/decorators/#egse.decorators.write_command","title":"write_command","text":"
write_command(func)\n

Adds a static variable __write_command to a method.

"},{"location":"api/exceptions/","title":"egse.exceptions","text":"
Exception\n \u251c\u2500\u2500 CGSEException\n \u2502    \u251c\u2500\u2500 Warning\n \u2502    \u2514\u2500\u2500 Error\n \u2502        \u251c\u2500\u2500 InvalidOperationError\n \u2502        \u251c\u2500\u2500 DeviceNotFoundError\n \u2502        \u251c\u2500\u2500 InternalStateError\n \u2502        \u2514\u2500\u2500 DeviceError\n \u2502             \u251c\u2500\u2500 DeviceControllerError\n \u2502             \u251c\u2500\u2500 DeviceConnectionError\n \u2502             \u251c\u2500\u2500 DeviceTimeoutError\n \u2502             \u2514\u2500\u2500 DeviceInterfaceError\n \u251c\u2500\u2500 Failure\n \u251c\u2500\u2500 HexapodError\n \u251c\u2500\u2500 PMACError\n \u251c\u2500\u2500 OGSEError\n \u251c\u2500\u2500 ESLError\n \u251c\u2500\u2500 FilterWheelError\n \u251c\u2500\u2500 FilterWheel8smc4Error\n \u251c\u2500\u2500 ShutterKSC1010Error\n \u251c\u2500\u2500 WindowSizeError\n \u251c\u2500\u2500 SettingsError\n \u2514\u2500\u2500 StagesError\n

Classes:

Name Description Abort

Internal Exception to signal a process to abort.

CGSEException

The base exception for all errors and warnings in the Common-EGSE.

DeviceNotFoundError

Raised when a device could not be located, or loaded.

Error

The base class for all Common-EGSE Errors.

FileIsEmptyError

Raised when a file is empty and that is unexpected.

InternalError

Raised when an internal inconsistency occurred in a function, method or class.

InternalStateError

Raised when an object encounters an internal state inconsistency.

InvalidInputError

Exception raised when the input is invalid after editing.

InvalidOperationError

Raised when a certain operation is not valid in the given state,

Warning

The base class for all Common-EGSE Warnings.

"},{"location":"api/exceptions/#egse.exceptions.Abort","title":"Abort","text":"

Bases: RuntimeError

Internal Exception to signal a process to abort.

"},{"location":"api/exceptions/#egse.exceptions.CGSEException","title":"CGSEException","text":"

Bases: Exception

The base exception for all errors and warnings in the Common-EGSE.

"},{"location":"api/exceptions/#egse.exceptions.DeviceNotFoundError","title":"DeviceNotFoundError","text":"

Bases: Error

Raised when a device could not be located, or loaded.

"},{"location":"api/exceptions/#egse.exceptions.Error","title":"Error","text":"

Bases: CGSEException

The base class for all Common-EGSE Errors.

"},{"location":"api/exceptions/#egse.exceptions.FileIsEmptyError","title":"FileIsEmptyError","text":"

Bases: Error

Raised when a file is empty and that is unexpected.

"},{"location":"api/exceptions/#egse.exceptions.InternalError","title":"InternalError","text":"

Bases: Error

Raised when an internal inconsistency occurred in a function, method or class.

"},{"location":"api/exceptions/#egse.exceptions.InternalStateError","title":"InternalStateError","text":"

Bases: Error

Raised when an object encounters an internal state inconsistency.

"},{"location":"api/exceptions/#egse.exceptions.InvalidInputError","title":"InvalidInputError","text":"

Bases: Error

Exception raised when the input is invalid after editing.

"},{"location":"api/exceptions/#egse.exceptions.InvalidOperationError","title":"InvalidOperationError","text":"

Bases: Error

Raised when a certain operation is not valid in the given state, circumstances or environment.

"},{"location":"api/exceptions/#egse.exceptions.Warning","title":"Warning","text":"

Bases: CGSEException

The base class for all Common-EGSE Warnings.

"},{"location":"api/settings/","title":"egse.settings","text":"

The Settings class handles user and configuration settings that are provided in a YAML file.

The idea is that settings are grouped by components or any arbitrary grouping that makes sense for the application or for the user. Settings are also modular and provided by each package by means of entry-points. The Settings class can read from different YAML files.

By default, settings are loaded from a file called settings.yaml, but this can be changed in the entry-point definition.

The yaml configuration files are provided as entry points by the packages that specified an entry-point group 'cgse.settings' in the pyproject.toml. The Settings dictionary (attrdict) is constructed from the configuration YAML files from each of the packages. Settings can be overwritten by the next package configuration file. So, make sure the group names in each package configuration file are unique.

The YAML file is read and the configuration parameters for the given group are available as instance variables of the returned class.

The intended use is as follows:

from egse.settings import Settings\n\ndsi_settings = Settings.load(\"DSI\")\n\nif 0x000C <= dsi_settings.RMAP_BASE_ADDRESS <= 0x00FF:\n    ...  # do something here\nelse:\n    raise RMAPError(\"Attempt to access outside the RMAP memory map.\")\n

The above code reads the settings from the default YAML file for a group called DSI. The settings will then be available as variables of the returned class, in this case dsi_settings. The returned class is and behaves also like a dictionary, so you can check if a configuration parameter is defined like this:

if \"DSI_FEE_IP_ADDRESS\" not in dsi_settings:\n    # define the IP address of the DSI\n

The YAML section for the above code looks like this:

DSI:\n\n    # DSI Specific Settings\n\n    DSI_FEE_IP_ADDRESS  10.33.178.144   # IP address of the DSI EtherSpaceLink interface\n    LINK_SPEED:                   100   # SpW link speed used for both up- and downlink\n\n    # RMAP Specific Settings\n\n    RMAP_BASE_ADDRESS:     0x00000000   # The start of the RMAP memory map managed by the FEE\n    RMAP_MEMORY_SIZE:            4096   # The size of the RMAP memory map managed by the FEE\n

When you want to read settings from another YAML file, specify the filename= keyword. If that file is located at a specific location, also use the location= keyword.

my_settings = Settings.load(filename=\"user.yaml\", location=\"/Users/JohnDoe\")\n

The above code will read the YAML file from the given location and not from the entry-points.

Classes:

Name Description Settings

The Settings class provides a load() method that loads configuration settings for a group

SettingsError

A settings-specific error.

Functions:

Name Description load_global_settings

Loads the settings that are defined by the given entry_point. The entry-points are defined in the

load_local_settings

Loads the local settings file that is defined from the environment variable PROJECT_LOCAL_SETTINGS (where

load_settings_file

Loads the YAML configuration file that is located at path / filename.

read_configuration_file

Read the YAML input configuration file. The configuration file is only read

"},{"location":"api/settings/#egse.settings.Settings","title":"Settings","text":"

The Settings class provides a load() method that loads configuration settings for a group into a dynamically created class as instance variables.

Methods:

Name Description load

Load the settings for the given group. When no group is provided, the

to_string

Returns a simple string representation of the cached configuration of this Settings class.

"},{"location":"api/settings/#egse.settings.Settings.load","title":"load classmethod","text":"
load(\n    group_name=None,\n    filename=\"settings.yaml\",\n    location=None,\n    *,\n    add_local_settings=True,\n    force=False,\n)\n

Load the settings for the given group. When no group is provided, the complete configuration is returned.

The Settings are loaded from entry-points that are defined in each of the packages that provide a Settings file.

If a location is explicitly provided, the Settings will be loaded from that location, using the given filename or the default (which is settings.yaml).

Parameters:

Name Type Description Default group_name str

the name of one of the main groups from the YAML file

None filename str

the name of the YAML file to read [default=settings.yaml]

'settings.yaml' location (str, Path)

the path to the location of the YAML file

None force bool

force reloading the file

False add_local_settings bool

update the Settings with site specific local settings

True

Returns:

Type Description attrdict

a dynamically created class with the configuration parameters as instance variables.

Raises:

Type Description SettingsError

when the group is not defined in the YAML file.

"},{"location":"api/settings/#egse.settings.Settings.to_string","title":"to_string classmethod","text":"
to_string()\n

Returns a simple string representation of the cached configuration of this Settings class.

"},{"location":"api/settings/#egse.settings.SettingsError","title":"SettingsError","text":"

Bases: Exception

A settings-specific error.

"},{"location":"api/settings/#egse.settings.load_global_settings","title":"load_global_settings","text":"
load_global_settings(\n    entry_point=\"cgse.settings\", force=False\n)\n

Loads the settings that are defined by the given entry_point. The entry-points are defined in the pyproject.toml files of the packages that export their global settings.

Parameters:

Name Type Description Default entry_point str

the name of the entry-point group [default: 'cgse.settings']

'cgse.settings' force bool

force reloading the settings, i.e. ignore the cache

False

Returns:

Type Description attrdict

A dictionary (attrdict) containing a collection of all the settings exported by the packages through the given entry-point.

"},{"location":"api/settings/#egse.settings.load_local_settings","title":"load_local_settings","text":"
load_local_settings(force=False)\n

Loads the local settings file that is defined from the environment variable PROJECT_LOCAL_SETTINGS (where PROJECT is the name of your project, defined in the environment variable of the same name).

This function might return an empty dictionary when

in both cases a warning message is logged.

Raises:

Type Description SettingsError

when the local settings YAML file is not found. Check the PROJECT_LOCAL_SETTINGS environment variable.

Returns:

Type Description attrdict

A dictionary (attrdict) with all local settings.

"},{"location":"api/settings/#egse.settings.load_settings_file","title":"load_settings_file","text":"
load_settings_file(path, filename, force=False)\n

Loads the YAML configuration file that is located at path / filename.

Parameters:

Name Type Description Default path PATH

the folder where the YAML file is located

required filename str

the name of the YAML configuration file

required force bool

force reloading, i.e. don't use the cached information

False

Raises:

Type Description SettingsError

when the configuration file doesn't exist or cannot be found or when there was an error reading the configuration file.

Returns:

Type Description attrdict

A dictionary (attrdict) with all the settings from the given file.

Note

in case of an empty configuration file, and empty dictionary is returned and a warning message is issued.

"},{"location":"api/settings/#egse.settings.read_configuration_file","title":"read_configuration_file","text":"
read_configuration_file(filename, *, force=False)\n

Read the YAML input configuration file. The configuration file is only read once and memoized as load optimization.

Parameters:

Name Type Description Default filename Path

the fully qualified filename of the YAML file

required force bool

force reloading the file, even when it was memoized

False

Raises:

Type Description SettingsError

when there was an error reading the YAML file.

Returns:

Type Description dict

a dictionary containing all the configuration settings from the YAML file.

"},{"location":"api/setup/","title":"egse.setup","text":""},{"location":"api/setup/#egse.setup--setup","title":"Setup","text":"

This module defines the Setup, which contains the complete configuration information for a test.

The Setup class contains all configuration items that are specific for a test or observation and is normally (during nominal operation/testing) loaded automatically from the configuration manager. The Setup includes type and identification of hardware that is used, calibration files, software versions, reference frames and coordinate systems that link positions of alignment equipment, conversion functions for temperature sensors, etc.

The configuration information that is in the Setup can be navigated in two different ways. First, the Setup is a dictionary, so all information can be accessed by keys as in the following example.

>>> setup = Setup({\"gse\": {\"hexapod\": {\"ID\": 42, \"calibration\": [0,1,2,3,4,5]}}})\n>>> setup[\"gse\"][\"hexapod\"][\"ID\"]\n42\n

Second, each of the keys is also available as an attribute of the Setup and that make it possible to navigate the Setup with dot-notation:

>>> id = setup.gse.hexapod.ID\n

In the above example you can see how to navigate from the setup to a device like the PUNA Hexapod. The Hexapod device is connected to the control server and accepts commands as usual. If you want to know which keys you can use to navigate the Setup, use the keys() method.

>>> setup.gse.hexapod.keys()\ndict_keys(['ID', 'calibration'])\n>>> setup.gse.hexapod.calibration\n[0, 1, 2, 3, 4, 5]\n

To get a full printout of the Setup, you can use the pretty_str() method. Be careful, because this can print out a lot of information when a full Setup is loaded.

>>> print(setup)\nSetup\n\u2514\u2500\u2500 gse\n    \u2514\u2500\u2500 hexapod\n        \u251c\u2500\u2500 ID: 42\n        \u2514\u2500\u2500 calibration: [0, 1, 2, 3, 4, 5]\n
"},{"location":"api/setup/#egse.setup--special-values","title":"Special Values","text":"

Some of the information in the Setup is interpreted in a special way, i.e. some values are processed before returning. Examples are the device classes and calibration/data files. The following values are treated special if they start with:

"},{"location":"api/setup/#egse.setup--device-classes","title":"Device Classes","text":"

Most of the hardware components in the Setup will have a device key that defines the class for the device controller. The device keys have a value that starts with class// and it will return the device object. As an example, the following defines the Hexapod device:

>>> setup = Setup(\n...   {\n...     \"gse\": {\n...       \"hexapod\": {\"ID\": 42, \"device\": \"class//egse.hexapod.symetrie.puna.PunaSimulator\"}\n...     }\n...   }\n... )\n>>> setup.gse.hexapod.device.is_homing_done()\nFalse\n>>> setup.gse.hexapod.device.info()\n'Info about the PunaSimulator...'\n

In the above example you see that we can call the is_homing_done() and info() methodes directly on the device by navigating the Setup. It would however be better (more performant) to put the device object in a variable and work with that variable:

>>> hexapod = setup.gse.hexapod.device\n>>> hexapod.homing()\n>>> hexapod.is_homing_done()\nTrue\n>>> hexapod.get_user_positions()\n

If you need, for some reason, to have access to the actual raw value of the hexapod device key, use the get_raw_value() method:

>>> setup.gse.hexapod.get_raw_value(\"device\")\n<egse.hexapod.symetrie.puna.PunaSimulator object at ...\n
"},{"location":"api/setup/#egse.setup--data-files","title":"Data Files","text":"

Some information is too large to add to the Setup as such and should be loaded from a data file. Examples are calibration files, flat-fields, temperature conversion curves, etc.

The Setup will automatically load the file when you access a key that contains a value that starts with csv// or yaml//.

>>> setup = Setup({\n...     \"instrument\": {\"coeff\": \"csv//cal_coeff_1234.csv\"}\n... })\n>>> setup.instrument.coeff[0, 4]\n5.0\n

Note: the resource location is always relative to the path defined by the PROJECT_CONF_DATA_LOCATION environment variable.

The Setup inherits from a NavigableDict (aka navdict) which is also defined in this module.

Classes:

Name Description Setup

The Setup class represents a version of the configuration of the test facility, the

SetupError

A setup-specific error.

Functions:

Name Description get_setup

Retrieve the currently active Setup from the configuration manager.

list_setups

This is a function to be used for interactive use, it will print to the terminal (stdout) a

load_last_setup_id

Returns the ID of the last Setup that was used by the configuration manager.

load_setup

This function loads the Setup corresponding with the given setup_id.

save_last_setup_id

Makes the given Setup ID persistent, so it can be restored upon the next startup.

submit_setup

Submit the given Setup to the Configuration Manager.

Attributes:

Name Type Description navdict

Shortcut for NavigableDict and more Pythonic.

"},{"location":"api/setup/#egse.setup.navdict","title":"navdict module-attribute","text":"
navdict = NavigableDict\n

Shortcut for NavigableDict and more Pythonic.

"},{"location":"api/setup/#egse.setup.NavigableDict","title":"NavigableDict","text":"
NavigableDict(head=None, label=None)\n

Bases: dict

A NavigableDict is a dictionary where all keys in the original dictionary are also accessible as attributes to the class instance. So, if the original dictionary (setup) has a key \"site_id\" which is accessible as setup['site_id'], it will also be accessible as setup.site_id.

Examples:

>>> setup = NavigableDict({'site_id': 'KU Leuven', 'version': \"0.1.0\"})\n>>> assert setup['site_id'] == setup.site_id\n>>> assert setup['version'] == setup.version\n
Note

We always want all keys to be accessible as attributes, or none. That means all keys of the original dictionary shall be of type str.

label (str): a label or name that is used when printing the navdict\n

Methods:

Name Description add

Set a value for the given key.

get_private_attribute

Returns the value of the given private attribute.

get_raw_value

Returns the raw value of the given key.

has_private_attribute

Check if the given key is defined as a private attribute.

pretty_str

Returns a pretty string representation of the dictionary.

set_private_attribute

Sets a private attribute for this object.

"},{"location":"api/setup/#egse.setup.NavigableDict.add","title":"add","text":"
add(key, value)\n

Set a value for the given key.

If the value is a dictionary, it will be converted into a NavigableDict and the keys will become available as attributes provided that all the keys are strings.

Parameters:

Name Type Description Default key str

the name of the key / attribute to access the value

required value Any

the value to assign to the key

required"},{"location":"api/setup/#egse.setup.NavigableDict.get_private_attribute","title":"get_private_attribute","text":"
get_private_attribute(key)\n

Returns the value of the given private attribute.

Parameters:

Name Type Description Default key str

the name of the private attribute (must start with an underscore character).

required

Returns:

Type Description Any

the value of the private attribute given in key.

Note

Because of the implementation, this private attribute can also be accessed as a 'normal' attribute of the object. This use is however discouraged as it will make your code less understandable. Use the methods to access these 'private' attributes.

"},{"location":"api/setup/#egse.setup.NavigableDict.get_raw_value","title":"get_raw_value","text":"
get_raw_value(key)\n

Returns the raw value of the given key.

Some keys have special values that are interpreted by the AtributeDict class. An example is a value that starts with 'class//'. When you access these values, they are first converted from their raw value into their expected value, e.g. the instantiated object in the above example. This method allows you to access the raw value before conversion.

"},{"location":"api/setup/#egse.setup.NavigableDict.has_private_attribute","title":"has_private_attribute","text":"
has_private_attribute(key)\n

Check if the given key is defined as a private attribute.

Parameters:

Name Type Description Default key str

the name of a private attribute (must start with an underscore)

required"},{"location":"api/setup/#egse.setup.NavigableDict.pretty_str","title":"pretty_str","text":"
pretty_str(indent=0)\n

Returns a pretty string representation of the dictionary.

Parameters:

Name Type Description Default indent int

number of indentations (of four spaces)

0 Note

The indent argument is intended for the recursive call of this function.

"},{"location":"api/setup/#egse.setup.NavigableDict.set_private_attribute","title":"set_private_attribute","text":"
set_private_attribute(key, value)\n

Sets a private attribute for this object.

The name in key will be accessible as an attribute for this object, but the key will not be added to the dictionary and not be returned by methods like keys().

The idea behind this private attribute is to have the possibility to add status information or identifiers to this classes object that can be used by save() or load() methods.

Parameters:

Name Type Description Default key str

the name of the private attribute (must start with an underscore character).

required value

the value for this private attribute

required

Examples:

>>> setup = NavigableDict({'a': 1, 'b': 2, 'c': 3})\n>>> setup.set_private_attribute(\"_loaded_from_dict\", True)\n>>> assert \"c\" in setup\n>>> assert \"_loaded_from_dict\" not in setup\n>>> assert setup.get_private_attribute(\"_loaded_from_dict\") == True\n
"},{"location":"api/setup/#egse.setup.Setup","title":"Setup","text":"
Setup(nav_dict=None, label=None)\n

Bases: NavigableDict

The Setup class represents a version of the configuration of the test facility, the test setup and the Camera Under Test (CUT).

Methods:

Name Description find_devices

Returns a dictionary with the devices that are included in the setup. The keys

from_dict

Create a Setup from a given dictionary.

from_yaml_file

Loads a Setup from the given YAML file.

from_yaml_string

Loads a Setup from the given YAML string.

get_filename

Returns the filename for this Setup or None when no filename could be determined.

get_id

Returns the Setup ID (as a string) or None when no setup id could be identified.

to_yaml_file

Saves a NavigableDict to a YAML file.

walk

Walk through the given dictionary, in a recursive way, appending the leaf with

"},{"location":"api/setup/#egse.setup.Setup.find_devices","title":"find_devices staticmethod","text":"
find_devices(node, devices=None)\n

Returns a dictionary with the devices that are included in the setup. The keys in the dictionary are taken from the \"device_name\" entries in the setup file. The corresponding values in the dictionary are taken from the \"device\" entries in the setup file.

Parameters:

Name Type Description Default node NavigableDict

Dictionary in which to look for the devices (and their names).

required devices dict

Dictionary in which to include the devices in the setup.

None

Returns:

Type Description dict

Dictionary with the devices that are included in the setup.

"},{"location":"api/setup/#egse.setup.Setup.from_dict","title":"from_dict staticmethod","text":"
from_dict(my_dict)\n

Create a Setup from a given dictionary.

Remember that all keys in the given dictionary shall be of type 'str' in order to be accessible as attributes.

Examples:

>>> setup = Setup.from_dict({\"ID\": \"my-setup-001\", \"version\": \"0.1.0\"})\n>>> assert setup[\"ID\"] == setup.ID == \"my-setup-001\"\n
"},{"location":"api/setup/#egse.setup.Setup.from_yaml_file","title":"from_yaml_file cached staticmethod","text":"
from_yaml_file(filename=None, add_local_settings=True)\n

Loads a Setup from the given YAML file.

Parameters:

Name Type Description Default filename str

the path of the YAML file to be loaded

None add_local_settings bool

if local settings shall be loaded and override the settings from the YAML file.

True

Returns:

Type Description

a Setup that was loaded from the given location.

"},{"location":"api/setup/#egse.setup.Setup.from_yaml_string","title":"from_yaml_string staticmethod","text":"
from_yaml_string(yaml_content=None)\n

Loads a Setup from the given YAML string.

This method is mainly used for easy creation of Setups from strings during unit tests.

Parameters:

Name Type Description Default yaml_content str

a string containing YAML

None

Returns:

Type Description

a Setup that was loaded from the content of the given string.

"},{"location":"api/setup/#egse.setup.Setup.get_filename","title":"get_filename","text":"
get_filename()\n

Returns the filename for this Setup or None when no filename could be determined.

"},{"location":"api/setup/#egse.setup.Setup.get_id","title":"get_id","text":"
get_id()\n

Returns the Setup ID (as a string) or None when no setup id could be identified.

"},{"location":"api/setup/#egse.setup.Setup.to_yaml_file","title":"to_yaml_file","text":"
to_yaml_file(filename=None)\n

Saves a NavigableDict to a YAML file.

When no filename is provided, this method will look for a 'private' attribute _filename and use that to save the data.

Parameters:

Name Type Description Default filename str | Path

the path of the YAML file where to save the data

None Note

This method will overwrite the original or given YAML file and therefore you might lose proper formatting and/or comments.

"},{"location":"api/setup/#egse.setup.Setup.walk","title":"walk staticmethod","text":"
walk(node, key_of_interest, leaf_list)\n

Walk through the given dictionary, in a recursive way, appending the leaf with the given keyword to the given list.

Parameters:

Name Type Description Default node dict

Dictionary in which to look for leaves with the given keyword.

required key_of_interest

Key to look for in the leaves of the given dictionary.

required leaf_list

List to which to add the leaves with the given keyword.

required

Returns:

Type Description list

Given list with the leaves (with the given keyword) in the given dictionary appended to it.

"},{"location":"api/setup/#egse.setup.SetupError","title":"SetupError","text":"

Bases: Exception

A setup-specific error.

"},{"location":"api/setup/#egse.setup.disentangle_filename","title":"disentangle_filename","text":"
disentangle_filename(filename)\n

Returns the site_id and setup_id (as a tuple) that is extracted from the Setups filename.

Parameters:

Name Type Description Default filename str

the filename or fully qualified file path as a string.

required

Returns:

Type Description tuple

A tuple (site_id, setup_id).

"},{"location":"api/setup/#egse.setup.get_last_setup_id_file_path","title":"get_last_setup_id_file_path","text":"
get_last_setup_id_file_path(site_id=None)\n

Return the fully expanded file path of the file containing the last loaded Setup in the configuration manager. The default location for this file is the data storage location.

Parameters:

Name Type Description Default site_id str

The SITE identifier (overrides the SITE_ID environment variable)

None"},{"location":"api/setup/#egse.setup.get_path_of_setup_file","title":"get_path_of_setup_file","text":"
get_path_of_setup_file(setup_id, site_id)\n

Returns the Path to the last Setup file for the given site_id. The last Setup file is the file with the largest setup_id number.

This function needs the environment variable _CONF_REPO_LOCATION to be defined as the location of the repository with configuration data on your disk. If the repo is not defined, the configuration data location will be used instead.

Parameters:

Name Type Description Default setup_id int

the identifier for the requested Setup

required site_id str

the test house name, one of CSL, SRON, IAS, INTA

required

Returns:

Type Description Path

The full path to the requested Setup file.

Raises:

Type Description LookupError

when the environment variable is not set.

NotADirectoryError

when either the repository folder or the Setups folder doesn't exist.

FileNotFoundError

when no Setup file can be found for the given arguments.

"},{"location":"api/setup/#egse.setup.get_setup","title":"get_setup","text":"
get_setup(setup_id=None)\n

Retrieve the currently active Setup from the configuration manager.

When a setup_id is provided, that setup will be returned, but not loaded in the configuration manager. This function does NOT change the configuration manager.

This function is for interactive use and consults the configuration manager server. Don't use this within the test script, but use the GlobalState.setup property instead.

"},{"location":"api/setup/#egse.setup.list_setups","title":"list_setups","text":"
list_setups(**attr)\n

This is a function to be used for interactive use, it will print to the terminal (stdout) a list of Setups known at the Configuration Manager. This list is sorted with the most recent ( highest) value last.

The list can be restricted with key:value pairs (keyword arguments). This search mechanism allows us to find all Setups that adhere to the key:value pairs, e.g. to find all Setups for CSL at position 2, use:

>>> list_setups(site_id=\"CSL\", position=2)\n

To have a nested keyword search (i.e. search by gse.hexapod.ID) then pass in gse__hexapod__ID as the keyword argument. Replace the '.' notation with double underscores '__'.

>>> list_setups(gse__hexapod__ID=4)\n
"},{"location":"api/setup/#egse.setup.load_last_setup_id","title":"load_last_setup_id","text":"
load_last_setup_id(site_id=None)\n

Returns the ID of the last Setup that was used by the configuration manager. The file shall only contain the Setup ID which must be an integer on the first line of the file. If no such ID can be found, the Setup ID = 0 will be returned.

Parameters:

Name Type Description Default site_id str

The SITE identifier

None"},{"location":"api/setup/#egse.setup.load_setup","title":"load_setup","text":"
load_setup(setup_id=None, site_id=None, from_disk=False)\n

This function loads the Setup corresponding with the given setup_id.

Loading a Setup means:

When no setup_id is provided, the current Setup is loaded from the configuration manager.

Parameters:

Name Type Description Default setup_id int

the identifier for the Setup

None site_id str

the name of the test house

None from_disk bool

True if the Setup needs to be loaded from disk

False

Returns:

Type Description Setup

The requested Setup or None when the Setup could not be loaded from the configuration manager.

"},{"location":"api/setup/#egse.setup.save_last_setup_id","title":"save_last_setup_id","text":"
save_last_setup_id(setup_id, site_id=None)\n

Makes the given Setup ID persistent, so it can be restored upon the next startup.

Parameters:

Name Type Description Default setup_id int | str

The Setup identifier to be saved

required site_id str

The SITE identifier

None"},{"location":"api/setup/#egse.setup.submit_setup","title":"submit_setup","text":"
submit_setup(setup, description)\n

Submit the given Setup to the Configuration Manager.

When you submit a Setup, the Configuration Manager will save this Setup with the next (new) setup id and make this Setup the current Setup in the Configuration manager unless you have explicitly set replace=False in which case the current Setup will not be replaced with the new Setup.

Parameters:

Name Type Description Default setup Setup

a (new) Setup to submit to the configuration manager

required description str

one-liner to help identifying the Setup afterwards

required

Returns:

Type Description str | None

The Setup ID of the newly created Setup or None.

"},{"location":"dev_guide/","title":"Developer Guide","text":"

Welcome to the CGSE developer guide! An in-depth reference on how to contribute to the CGSE.

First thing to know is that this repository is actually a monorepo, meaning it contains a bunch of related but self-standing packages with a minimum of interdependencies. A monorepo can grow quite big and can contain a lot of packages that even different groups are working on. What they have in common is that they use the same guidelines and have the same or a very similar development workflow.

Don't confuse a monorepo with a monolith or a monolithic architecture. While a monorepo holds multiple related but more-or-less independent projects, a monolith is a traditional software application or architecture which is an often huge, self-contained and independent unit of code that is highly coupled and difficult to maintain.

Don't confuse a monorepo with microservices either. A microservice architecture contains units that run independently and are developed, scaled and deployed without affecting the other units or services. You can set up a monorepo containing all of your microservices with ease, one does not need the other, but they can perfectly go together.

"},{"location":"dev_guide/coding_style/","title":"Style Guide","text":"

This part of the developer guide contains instructions for coding styles that are adopted for this project.

The style guide that we use for this project is PEP8. This is the standard for Python code and all IDEs, parsers and code formatters understand and work with this standard. PEP8 leaves room for project specific styles. A good style guide that we can follow is the Google Style Guide.

The following sections will give the most used conventions with a few examples of good and bad.

"},{"location":"dev_guide/coding_style/#tldr","title":"TL;DR","text":"Type Style Example Classes CapWords ProcessManager, ImageViewer, CommandList, Observation, MetaData Methods & Functions lowercase with underscores get_value, set_mask, create_image Variables lowercase with underscores key, last_value, model, index, user_info Constants UPPERCASE with underscores MAX_LINES, BLACK, COMMANDING_PORT Modules & packages lowercase no underscores dataset, commanding, multiprocessing"},{"location":"dev_guide/coding_style/#general","title":"General","text":"

Note

You will sometimes see that we use one or two words between < > angle brakcets. That means you will have to replace that text AND the brackets with your own text. As an example, if you see --prompt <venv name>, replace this with something like --prompt cgse-venv.

"},{"location":"dev_guide/coding_style/#classes","title":"Classes","text":"

Always use CamelCase (Python uses CapWords) for class names. When using acronyms, keep them all UPPER case.

Good names are: Observation, CalibrationFile, MetaData, Message, ReferenceFrame, URLParser.

"},{"location":"dev_guide/coding_style/#methods-and-functions","title":"Methods and Functions","text":"

A function or a method does something (and should only do one thing, SRP=Single Responsibility Principle), it is an action, so the name should reflect that action.

Always use lowercase words separated with underscores.

Good names are: get_time_in_ms(), get_commanding_port(), is_connected(), parse_time(), setup_mask().

When working with legacy code or code from another project, names may be in camelCase (with the first letter a lower case letter). So we can in this case use also getCommandPort() or isConnected() as method and function names.

"},{"location":"dev_guide/coding_style/#variables","title":"Variables","text":"

Use the same naming convention as functions and methods, i.e. lowercase with underscores.

Good names are: key, value, user_info, model, last_value

Bad names: NSegments, outNoise

Take care not to use builtins: list, type, filter, lambda, map, dict, ...

Private variables (for classes) start with an underscore: _name or _total_n_args.

In the same spirit as method and function names, the variables can also be in camelCase for specific cases.

"},{"location":"dev_guide/coding_style/#constants","title":"CONSTANTS","text":"

Use ALL_UPPER_CASE with underscores for constants. Use constants always within a name space, not globally.

Good names: MAX_LINES, BLACK, YELLOW, ESL_LINK_MODE_DISABLED

"},{"location":"dev_guide/coding_style/#modules-and-packages","title":"Modules and Packages","text":"

Use simple words for modules, preferably just one word like datasets or commanding or storage or extensions. If two words are unavoidable, just concatenate them, like multiprocessing or sampledata or testdata. If needed for readability, use an underscore to separate the words, e.g. image_analysis.

"},{"location":"dev_guide/coding_style/#import-statements","title":"Import Statements","text":"

Be careful that you do not name any modules the same as a module in the Python standard library. This can result in strange effects and may result in an AttributeError. Suppose you have named a module math in the egse directory and it is imported and used further in the code as follows:

from egse import math\n\n# in some expression further down the code you might use\n\nmath.exp(a)\n

This will result in the following runtime error:

File \"some_module.py\", line 8, in <module>\n  print(math.exp(a))\nAttributeError: module 'egse.math' has no attribute 'exp'\n

Of course this is an obvious example, but it might be more obscure like e.g. in this GitHub issue: 'module' object has no attribute 'Cmd'.

"},{"location":"dev_guide/docs/","title":"Building the documentation","text":""},{"location":"dev_guide/docs/#set-up-your-environment","title":"Set up your environment","text":"

The pyproject.toml file of the cgse root contains additional dependencies for running the mkdocs commands. When working on the documentation, make sure you have installed the 'docs' dependency group. Currently, only mkdocs and mkdocs-material are needed. You can use the following command to add the documentation dependencies to your development environment.

$ cd ~/github/cgse\n$ uv sync --all-packages --all-groups\n

Now you can start the live-reload server of mkdocs. This will recreate the documentation whenever you make a change in the files below the docs folder. After starting this command, navigate to the http://127.0.0.1:8000/cgse/ site in your favorite browser.

$ uv run mkdocs serve\n

Now you can update files, create new folders in docs/*, create new Markdown files and all changes will be reloaded live in the browser.

When you are ready with updating, you will need to build the site and publish it on GitHub pages:

$ uv run mkdocs build\n$ uv run mkdocs gh-deploy -r upstream -m \"documentation update on ..\"\n
"},{"location":"dev_guide/docs/#commands","title":"Commands","text":""},{"location":"dev_guide/docs/#project-layout","title":"Project layout","text":"

The documentation pages follow more or less the structure of the code in terms of libs and projects. Below I have laid out this structure leaving out less important files and folders.

mkdocs.yml         # the mkdocs configuration file\ndocs\n\u251c\u2500\u2500 index.md       # the documentation homepage\n\u251c\u2500\u2500 initialize.md\n\u251c\u2500\u2500 getting_started.md\n\u251c\u2500\u2500 package_list.md\n\u251c\u2500\u2500 dev_guide/\n\u251c\u2500\u2500 user_guide/\n\u251c\u2500\u2500 libs\n\u2502   \u251c\u2500\u2500 cgse-common/\n\u2502   \u251c\u2500\u2500 cgse-coordinates/\n\u2502   \u251c\u2500\u2500 cgse-core/\n\u2502   \u251c\u2500\u2500 cgse-gui/\n\u2502   \u2514\u2500\u2500 index.md\n\u251c\u2500\u2500 projects/\n\u2502   \u251c\u2500\u2500 cgse-tools.md\n\u2502   \u251c\u2500\u2500 symetrie-hexapod.md\n\u2502   \u2514\u2500\u2500 index.md\n\u251c\u2500\u2500 images/\n\u2514\u2500\u2500 roadmap.md\n
"},{"location":"dev_guide/installation/","title":"Installation Guide for Developers","text":""},{"location":"dev_guide/installation/#github","title":"GitHub","text":"

Before starting, make sure you have a fork of the cgse repository. Through this fork (which resides on the GitHub server) you will create pull requests. Install a clone of your fork on your local machine or laptop.

So, when you have created a fork in your GitHub account, clone the repository on your local machine. For the purpose of this guide we will clone the repo in the ~/github/cgse folder. The following commands will create the required folders and clone the repo.

$ mkdir -p ~/github\n$ cd ~/github\n$ git clone git@github.com:IvS-KULeuven/cgse.git\n$ cd ~/github/cgse\n

Now you will have to create a virtual environment and populated it with all the dependencies.

Note

The following three commands will get you going quickly:

$ uv venv --python 3.9.20\n$ uv sync --all-packages\n$ uv run cgse\n\nUsage: cgse [OPTIONS] COMMAND [ARGS]...\n\nThe main cgse command to inspect, configure, monitor the core services and device control \nservers.\n\n\u256d\u2500 Options \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 --install-completion          Install completion for the current shell.                                        \u2502\n\u2502 --show-completion             Show completion for the current shell, to copy it or customize the installation. \u2502\n\u2502 --help                        Show this message and exit.                                                      \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\u256d\u2500 Commands \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 version   Prints the version of the cgse-core and other registered packages.                                   \u2502\n\u2502 top       A top-like interface for core services and device control servers.                                   \u2502\n\u2502 clock     Showcase for running an in-line Textual App.                                                         \u2502\n\u2502 init      Initialize your project.                                                                             \u2502\n\u2502 show      Show information about settings, environment, setup, ...                                             \u2502\n\u2502 check     Check installation, settings, required files, etc.                                                   \u2502\n\u2502 dev-x     device-x is an imaginary device that serves as an example                                            \u2502\n\u2502 core      handle core services: start, stop, status                                                            \u2502\n\u2502 puna      PUNA Positioning Hexapod, Sym\u00e9trie                                                                   \u2502\n\u2502 daq6510   DAQ6510 Data Acquisition Unit, Keithley, temperature monitoring                                      \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n

"},{"location":"dev_guide/monorepo/","title":"The structure of this monorepo","text":"

Currently, the structure starts with two main folders in the root, i.e. libs and projects. Where libs contains library type packages like common modules, small generic gui and tui functions, reference frames, ... and projects contain packages that build upon these libraries and can be device drivers or stand-alone applications.

There is one package that I think doesn't fit into this picture, that is cgse-core. This is not a library, but a \u2013 collection of \u2013 service(s). So, we might want to add a third top-level folder services but I also fear that this again more complicates the monorepo.

Anyway, the overall structure of the monorepo is depicted below:

cgse/\n\u2502\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 libs/\n\u2502   \u251c\u2500\u2500 cgse-common/\n\u2502   \u2502   \u251c\u2500\u2500 src/\n\u2502   \u2502   \u251c\u2500\u2500 tests/\n\u2502   \u2502   \u2514\u2500\u2500 pyproject.toml\n\u2502   \u251c\u2500\u2500 cgse-core/\n\u2502   \u2502   \u251c\u2500\u2500 src/\n\u2502   \u2502   \u251c\u2500\u2500 tests/\n\u2502   \u2502   \u2514\u2500\u2500 pyproject.toml\n\u2502   \u251c\u2500\u2500 cgse-coordinates/\n\u2502   \u2502   \u251c\u2500\u2500 src/\n\u2502   \u2502   \u251c\u2500\u2500 tests/\n\u2502   \u2502   \u2514\u2500\u2500 pyproject.toml\n\u2502   \u2514\u2500\u2500 cgse-gui/\n\u2502   \u2502   \u251c\u2500\u2500 src/\n\u2502   \u2502   \u251c\u2500\u2500 tests/\n\u2502   \u2502   \u2514\u2500\u2500 pyproject.toml\n\u2502\n\u2514\u2500\u2500 projects/\n    \u251c\u2500\u2500 generic/\n    \u2502   \u251c\u2500\u2500 cgse-tools/\n    \u2502   \u251c\u2500\u2500 keithley-tempcontrol/\n    \u2502   \u2514\u2500\u2500 symetrie-hexapod/\n    \u2514\u2500\u2500 plato/\n        \u251c\u2500\u2500 plato-spw/\n        \u251c\u2500\u2500 plato-fits/\n        \u2514\u2500\u2500 plato-hdf5/\n

We will discuss the structure of individual packages in a later section, for now let's look at the root of the monorepo. The root also contains a pyproject.toml file although this is not a package that will be build and published. The purpose of this root pyproject.toml file is to define properties that are used to build the full repo or any individual package in it. In the root folder we will also put some maintenance/management scripts to help you maintain and bump versions of the projects, build and publish all projects, create and maintain a changelog etc.

"},{"location":"dev_guide/monorepo/#package-structure","title":"Package Structure","text":"

We try to keep the package structure as standard as possible and consistent over the whole monorepo. The structure currently is as follows (example from cgse-common):

\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 src/\n\u2502   \u2514\u2500\u2500 egse/  # namespace, i.e. there shall not be a __init__.py in this folder\n\u2502       \u251c\u2500\u2500 modules (*.py)\n\u2502       \u2514\u2500\u2500 <sub-packages>/  # these do contain a __init__.py\n\u2514\u2500\u2500 tests/\n    \u251c\u2500\u2500 data\n    \u2514\u2500\u2500 pytest modules (test_*.py)\n

Note that each library or project is a standalone Python package with its own pyproject.toml file, source code and unit tests.

"},{"location":"dev_guide/monorepo/#package-versions","title":"Package versions","text":"

All packages in the monorepo will have the same version. This can be maintained with the bump.py script. This script will read the version from the pyproject.toml file at the root of the monorepo and propagate the version to all libs and projects in the monorepo. Note that you \u2013for now\u2013 will have to update the version number in the pyproject.toml file located at the monorepo root folder manually.

"},{"location":"dev_guide/monorepo/#the-egse-namespace","title":"The egse namespace","text":"

You might have notices that all packages in this monorepo have a src/egse folder in which they maintain their source code, preferably in a sub-package. Note that the egse folder is not a normal Python package but a namespace. There are two important facts you need to remember about namespaces:

  1. A namespace package does not contain an __init__.py module, never, in any of the packages in this or any other repo. If you place an __init__.py module in one of your egse package folders, you will break the namespace and therefore also the external contributions in plugins etc.
  2. A namespace package is spread out over several directories that can reside in different packages as distributed by PyPI.
"},{"location":"dev_guide/monorepo/#egse-versus-cgse","title":"egse versus cgse","text":"

Why is there sometimes egse and sometimes cgse used in documentation, folder names etc.? The acronym EGSE stands for Electric Ground Support Equipment and the CGSE stands for Common-EGSE. So, the latter, CGSE, is what we use for the project name, to emphasise its common purpose as a framework for testing instrumentation and for external packages and device drivers to emphasise that they are intended to be common and work well with the CGSE framework. The egse is what the software is about, the electric ground support equipment, and therefore we use this for the namespace, i.e. the root of the library and projects. Using egse as the namespace also avoid any conflicts with the cgse monorepo name.

"},{"location":"dev_guide/plugins/","title":"Plugins","text":"

The CGSE is designed to be extensible and uses a few plugin mechanisms to extend its functionally with external contributions. Also within the cgse monorepo we use the plugin mechanism at several places. The following entry-points are currently defined:

Each of the entry-points knows how to load a module or object and each entry-point group is connected to a specific action or plugin hook like, e.g. add a command or command group to the cgse app, add package specific settings to the global settings.

"},{"location":"dev_guide/plugins/#version-discovery","title":"Version discovery","text":"

When you write a package that you want to integrate with the CGSE, provide a cgse.version entry-point. The name of the entry-point shall match the package name and is used to read the version from the importlib metadata. The entry-point value is currently not used. The entry-point value can optionally provide additional information about the package, but that is currently not specified.

Add the following to your pyproject.toml file in your project's root folder, replacing package-name with the name of your project. The entry-point value is currently not used, but you want to use a valid format, the value below is always valid.

[project.entry-points.\"cgse.version\"]\npackage-name = 'egse.version:get_version_installed'\n
"},{"location":"dev_guide/plugins/#extending-the-cgse-app","title":"Extending the cgse app","text":""},{"location":"dev_guide/plugins/#add-a-command","title":"Add a Command","text":"

If your package provides specific functionality that can be added as a command or a command group to the cgse app, use the cgse.command entry-point group. Since the cgse app uses the Typer package to build its commandline interface, adding a command is as simple as writing a function. The function will be added to the cgse app using the app.command() function of Typer, making the function a top-level command of the cgse app. The function can be defined as a plain function or with Typer's @app.command decorator.

In the pyproject.toml file of your project, add the following lines to add the CGSE command:

[project.entry-points.\"cgse.command\"]\nname = 'module:object'\n

Where:

As an example, for the cgse-tools package, the init command of the cgse app is listed in the pyproject.toml file as follows:

[project.entry-points.\"cgse.command\"]\ninit = 'cgse_tools.cgse_commands:init'\n

The init function is defined in the cgse_commands.py module which is located in the cgse_tools module in the src folder of the package:

src\n\u251c\u2500\u2500 cgse_tools\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u251c\u2500\u2500 cgse_commands.py\n...\n
"},{"location":"dev_guide/plugins/#add-a-command-group","title":"Add a Command group","text":"

Some commands are more complicated and define a number of sub-commands. An example is the show command where you currently have the sub-commands env and settings

$ cgse show --help\n\n Usage: cgse show [OPTIONS] COMMAND [ARGS]...\n\n Show information about settings, environment, setup, ...\n\n\u256d\u2500 Options \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 --help          Show this message and exit.                                           \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\u256d\u2500 Commands \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 settings   Show the settings that are defined by the installed packages.              \u2502\n\u2502 env        Show the environment variables that are defined for the project.           \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n

The show command is defined as a typer.Typer() object where env and settings are added using the decorator @<app>.command().

import typer\n\nshow = typer.Typer(help=\"Show information about settings, environment, setup, ...\")\n\n\n@show.command(name=\"settings\")\ndef show_settings():\n    ...\n\n\n@show.command(name=\"env\")\ndef show_env():\n    ...\n

To add this command group to the cgse app, the following entry was used in the pyproject. toml file of the cgse-tools project. Notice the [group] at the end of the entry which indicates this is a command group instead of a single command.

[project.entry-points.\"cgse.command\"]\nshow = 'cgse_tools.cgse_commands:show[group]'\n
"},{"location":"dev_guide/plugins/#add-a-service","title":"Add a Service","text":"

If your package provides a device driver or a specific service, use the cgse.service entry-point group. Service entry-points follow the same scheme as command groups, i.e. they are added to the cgse app as a Typer() object. Use the following entry in your pyproject.toml file:

[project.entry-points.\"cgse.service\"]\nname = 'module:object'\n

where:

"},{"location":"dev_guide/plugins/#explore","title":"Explore","text":"

The entry-point cgse.explore can be used to extend functionality without adding a new command or sub-command to the cgse app. The idea is that commands that work on different packages can use this entry-point to perform certain tasks on the package. This is currently used for the show procs command (see below).

The entry-point has the following format:

[project.entry-points.\"cgse.explore\"]\nexplore = \"<package>.cgse_explore\"\n

So, what happens is that a command that wants to apply a functionality on an external package loads the cgse_explore.py module for that package and checks if a function with a specific name exists in that module. It then executes that function. For the show procs command, the function show_processes is expected and it shall return a list of strings which currently are printed to the terminal. This entry-point is currently implemented for cgse-core and cgse-dummy (an external demo package) and when you run the cgse show procs command it looks something like below (the format is from the unix ps -ef command).

\u279c  cgse show procs\n459800007 76849     1   0 11:07PM ttys003    0:03.53 /Users/rik/tmp/test_dummy/venv/bin/python3.9 -m egse.logger.log_cs start\n459800007 76850     1   0 11:07PM ttys003    2:18.60 /Users/rik/tmp/test_dummy/venv/bin/python3.9 -m egse.storage.storage_cs start\n459800007 76851     1   0 11:07PM ttys003    2:20.10 /Users/rik/tmp/test_dummy/venv/bin/python3.9 -m egse.confman.confman_cs start\n459800007 13825     1   0  4:31PM ttys003    0:02.97 /Users/rik/tmp/test_dummy/venv/bin/python3.9 -m cgse_dummy.dummy_sim start\n
"},{"location":"dev_guide/plugins/#register-resources","title":"Register resources","text":"

TODO: what if two packages provide a resource icons ?

"},{"location":"dev_guide/unit_testing/","title":"Testing the Software","text":"

We use the pytest package to unit test our modules and packages. The pyproject.toml files are configured for each package to perform the testing. This section will guide you through the steps to run the tests and also explain how we configured the tests and some guidelines we used.

"},{"location":"dev_guide/unit_testing/#running-the-unit-test-for-each-package-separately","title":"Running the unit test for each package separately","text":"

If you are working on a particular package and want to run its unit test, make sure you are in the root folder of that package, e.g. for the cgse-common package, do the following:

$ cd ~/github/cgse/libs/cgse-common/\n$ uv sync\n$ uv run pytest -v\n================================================================ test session starts =================================================================\nplatform darwin -- Python 3.9.20, pytest-8.3.4, pluggy-1.5.0 -- /Users/rik/github/cgse/.venv/bin/python3\ncachedir: .pytest_cache\nrootdir: /Users/rik/github/cgse/libs/cgse-common\nconfigfile: pyproject.toml\nplugins: cov-6.0.0, mock-3.14.0\ncollected 161 items\n\ntest_bits.py::test_clear_bit PASSED                                                                                                            [  0%]\ntest_bits.py::test_set_bit PASSED                                                                                                              [  1%]\ntest_bits.py::test_toggle_bit PASSED                                                                                                           [  1%]\ntest_bits.py::test_beautify_binary PASSED                                                                                                      [  2%]\ntest_bits.py::test_set_bits PASSED                                                                                                             [  3%]\ntest_bits.py::test_alternative_set_bits PASSED                                                                                                 [  3%]\ntest_bits.py::test_clear_bits PASSED                                                                                                           [  4%]\ntest_bits.py::test_crc_calc PASSED                                                                                                             [  4%]\ntest_bits.py::test_humanize_bytes PASSED                                                                                                       [  5%]\ntest_bits.py::test_s16 PASSED                                                                                                                  [  6%]\ntest_bits.py::test_s32 PASSED                                                                                                                  [  6%]\ntest_command.py::test_dry_run PASSED                                                                                                           [  7%]\ntest_command.py::test_command_class PASSED                                                                                                     [  8%]\ntest_command.py::test_return_code_of_execute PASSED                                                                                            [  8%]\n...\n
"},{"location":"dev_guide/unit_testing/#running-the-unit-tests-of-all-packages","title":"Running the unit tests of all packages","text":"

Before releasing the software, we should run all the unit tests of all the packages in the monorepo and have green light \ud83d\udfe2. Running these unit tests is as simple as for the individual packages. You will need to be in the root folder of the monorepo and sync your virtual environment for all the packages in the workspace.

$ cd ~/gitbug/cgse\n$ uv sync --all-packages\n$ uv run pytest -v\n
"},{"location":"dev_guide/uv/","title":"Working with uv","text":"

uv is an extremely fast Python package and project manager, written in Rust. We will use uv as the single tool that replaces pip, virtualenv, pyenv, and more. The main tasks for which we will use uv are:

"},{"location":"dev_guide/uv/#installing-uv","title":"Installing uv","text":"

On macOS and Linux you can install uv using curl:

$ curl -LsSf https://astral.sh/uv/install.sh | sh\n

If you need more specific information on installing and upgrading uv, please refer to the official documentation.

"},{"location":"dev_guide/uv/#installing-a-python-version","title":"Installing a Python version","text":"

The CGSE is guaranteed to work with Python 3.9.x. We will gradually include higher versions of Python, but currently these have not been tested. So, we will for the moment stick with Python 3.9.20. Install this version as follows:

$ uv python install 3.9.20\n

pyenv

When you are using pyenv to manage your Python versions, make sure you also have the same Python version installed with pyenv and uv. Otherwise you will run into the following error. This is a known issue with uv.

pyenv: version `3.9.20' is not installed (set by /Users/rik/github/cgse/libs/cgse-common/.python-version)\n

You can check which Python versions are installed already on your system:

CommandOutput
$ uv python list --only-installed\n
cpython-3.12.8-macos-aarch64-none     /Users/rik/Library/Application Support/uv/python/cpython-3.12.8-macos-aarch64-none/bin/python3.12\ncpython-3.10.16-macos-aarch64-none    /Users/rik/Library/Application Support/uv/python/cpython-3.10.16-macos-aarch64-none/bin/python3.10\ncpython-3.9.21-macos-aarch64-none     /Users/rik/Library/Application Support/uv/python/cpython-3.9.21-macos-aarch64-none/bin/python3.9\ncpython-3.9.20-macos-aarch64-none     /Users/rik/Library/Application Support/uv/python/cpython-3.9.20-macos-aarch64-none/bin/python3.9\ncpython-3.9.6-macos-aarch64-none      /Library/Developer/CommandLineTools/usr/bin/python3 -> ../../Library/Frameworks/Python3.framework/Versions/3.9/bin/python3\ncpython-3.8.17-macos-aarch64-none     /Users/rik/Library/Application Support/uv/python/cpython-3.8.17-macos-aarch64-none/bin/python3.8\n
"},{"location":"dev_guide/uv/#create-a-virtual-environment","title":"Create a virtual environment","text":"

Pin a Python version

You can pin a python version with the command:

$ uv python pin 3.9.20\n

uv will search for a pinned version in the parent folders up to the root folder or your home directory.

You can create a virtual environment with uv for the specific Python version as follows. The '--python' is optional and uv will use the default (pinned) Python version when creating a venv without this option. We are working in a monorepo or what uv calls a workspace. There will be only one virtual environment at the root of the monorepo, despite the fact that we have several individual packages in our workspace. Don't worry, uv will always use the virtual environment at the root and keep it up-to-date with the project your are currently working in.

When creating a virtual environment make sure you are in the root folder, e.g. ~/github/cgse.

$ cd ~/github/cgse\n$ uv venv --python 3.9.20\n
If you want to name your virtual environment, use the optional argument --prompt <venv name> in the above command, otherwise the virtual environment will get the same name as the project, i.e. cgse.

Now, navigate to the package you will be working in and update the projects' environment, assuming you are going to work in cgse-core, this will be:

$ cd ~/github/cgse/libs/cgse-core\n$ uv sync\n

Your package(s) from the workspace should be installed as an editable install. You can check this with the command:

$ uv pip list -v\nUsing Python 3.9.20 environment at: /Users/rik/github/cgse/.venv\nPackage           Version     Editable project location\n----------------- ----------- ---------------------------------------\napscheduler       3.11.0\ncgse-common       0.4.0       /Users/rik/github/cgse/libs/cgse-common\ncgse-core         0.4.0       /Users/rik/github/cgse/libs/cgse-core\n...\n

To install any other project as an editable package:

$ uv pip install -e <project location>\n

Note

If you don't want to use the uv commands, you can activate the virtual environment and use the original pip and python commands as you are used to, but I would recommend you try to get used to uv for a while to experience its benefits.

$ source ~/github/cgse/.venv/bin/activate\n

Info

In a workspace, maintaining a virtual environment per package might be a hassle and most of the time that is not needed. A good approach is to always use the virtual environment at the workspace root. This venv which will be automatically created if you run a command or if you use uv sync in the package folder. With uv sync you can make sure the virtual environment is up-to-date and contains only those dependencies that are required for the package you are in. So, each time you switch to another package and want to run a comand or a test for that package, use

$ uv sync\n
"},{"location":"dev_guide/uv/#building-and-publishing-all-packages","title":"Building and publishing all packages","text":"

We have chosen for one and the same version number for all packages in the cgse monorepo. That means that whenever we make a change to one of the packages and want to release that change, all packages shall be rebuild and published.

Inline

When working in a workspace, keep in mind that the commands uv run and uv sync by default work on the workspace root. That means that when you run the uv run pip install <package> command, the .venv at the workspace root will be updated or created if it didn't exist. Similar for the uv sync command, there is only one uv.lock file at the root of the workspace.

Fortunately, with uv, that is done in a few commands.

When you are in the monorepo root folder, you can build all packages at once. They will be placed in the dist folder of the root package. Before building, make sure you update the version in the pyproject.toml of the root package and then bump the versions. Before building, clean up the dist folder, then you can do a default uv publish afterwards.

$ cd <monorepo root>\n$ uv run bump.py\n$ rm -r dist\n$ uv build --all-packages\n

Publish all packages in the root dist folder to PyPI. The UV_PUBLISH_TOKEN can be defined in a (read protected) ~/. setenv.bash file:

$ uv publish --token $UV_PUBLISH_TOKEN\n

The above command will publish all package to PyPI. If you don't want the token to be in a shell variable, you can omit the --token in the command above. You will then be asked for a username, use __token__ as the username and then provide the token as a password.

"},{"location":"dev_guide/versioning/","title":"Semantic Versioning","text":"

We use semantic versioning, aka semver, for our releases and patches. Please follow the rules that are described on their site.

TL;DR

The version number has the format MAJOR.MINOR.PATH, we increment the

The rules above apply when MAJOR >= 1, which are considered stable releases.

As long as MAJOR == 0, we are in initial development and anything may change. The MINOR number will be increased for adding or removing functionality and the PATCH number will be increased for all kinds of fixes.

You might occasionally see pre-release and build metadata added to the version number. We will use the following metadata:

"},{"location":"dev_guide/versioning/#why-not-calver","title":"Why not CalVer?","text":"

We do not use Calendar Versioning for the following reason:

"},{"location":"libs/","title":"Libraries","text":"

The libraries are those packages that make up the CGSE framework.

The libraries are located under the libs folder, and we currently find the following packages there:

"},{"location":"libs/cgse-common/","title":"Common Code","text":"

This package cgse-common contains modules that are used by all other packages.

Module Name Description egse.bits convenience functions to work with bits, bytes and integers egse.calibration functions to handle conversions and apply correction egse.command classes and functions to work with commands that operate hardware devices egse.config convenience functions to configure the system and find folders and files egse.control defines abstract classes and convenience functions for any control server egse.decorators a collection of useful decorator functions egse.device defines the generic interfaces to connect devices egse.env functionality to work with and check your environment variables egse.exceptions common Exceptions and Errors egse.hk functions to retrieve and convert housekeping parameter values egse.metrics functions to define and update metrics egse.mixin defines the mixin classes for dynamic commanding egse.monitoring the monitoring application / function egse.observer the classic observer and observable egse.obsid functions to define and work with the OBSID egse.persistence the persistence layer interface egse.plugin functions to load plugins and settings from entry-points egse.process functions and classes to work with processes and sub-processes egse.protocol base class for communicating commands with the hardware or the control server egse.proxy base class for the Proxy objects for each device controller egse.reload a slightly better approach to reloading modules and function egse.resource convenience functions to use resources in your code egse.response defines the classes to handle responses from the control servers egse.services provides the services to the control servers egse.settings provides functions to handle user and configuration settings egse.setup defines the Setup, containing the complete configuration for a test egse.state classes and functions to handle state, e.g. the GlobalState egse.system convenience functions that provide information on system specific functionality egse.version functions to load specific version information egse.zmq_ser serialization function used in a ZeroMQ context"},{"location":"libs/cgse-common/settings/","title":"The Settings","text":"

The Settings class contains all static information needed to configure your system, the environment you are using and the test equipment. The Settings also contain all the IP addresses and port number for all the known devices, together with other static information like the device name, default settings for the device like speed, timeout, delay time, firmware version, etc. We will go into more details about the content later, let\u2019s now first look at the format and usage of the Settings.

"},{"location":"libs/cgse-common/settings/#loading-the-settings","title":"Loading the Settings","text":"

The Settings can be loaded as follows:

>>> from egse.settings import Settings\n>>> settings = Settings.load()\n

The settings object will be a dictionary where the keys are the top-level groups that are defined in the settings for each package. For a system that has only cgse-common and cgse-core installed, the settings will contain something like this:

>>> print(settings)\nSettings\n\u251c\u2500\u2500 PACKAGES\n\u2502   \u251c\u2500\u2500 CGSE_COMMON: Common classes, functions, decorators, etc. for the CGSE\n\u2502   \u2514\u2500\u2500 CGSE_CORE: The core services of the CGSE\n\u251c\u2500\u2500 SITE\n\u2502   \u251c\u2500\u2500 ID: LAB42\n\u2502   \u251c\u2500\u2500 SSH_SERVER: localhost\n\u2502   \u2514\u2500\u2500 SSH_PORT: 22\n\u251c\u2500\u2500 PROCESS\n\u2502   \u2514\u2500\u2500 METRICS_INTERVAL: 10\n\u251c\u2500\u2500 Logging Control Server\n\u2502   \u251c\u2500\u2500 PROTOCOL: tcp\n\u2502   \u251c\u2500\u2500 HOSTNAME: localhost\n\u2502   \u251c\u2500\u2500 LOGGING_PORT: 7000\n\u2502   \u251c\u2500\u2500 COMMANDING_PORT: 7001\n\u2502   \u251c\u2500\u2500 METRICS_PORT: 7003\n\u2502   \u251c\u2500\u2500 MAX_NR_LOG_FILES: 20\n\u2502   \u251c\u2500\u2500 MAX_SIZE_LOG_FILES: 20\n\u2502   \u251c\u2500\u2500 TEXTUALOG_IP_ADDRESS: 127.0.0.1\n\u2502   \u2514\u2500\u2500 TEXTUALOG_LISTENING_PORT: 19996\n\u251c\u2500\u2500 Configuration Manager Control Server\n\u2502   \u251c\u2500\u2500 PROTOCOL: tcp\n\u2502   \u251c\u2500\u2500 HOSTNAME: localhost\n\u2502   \u251c\u2500\u2500 COMMANDING_PORT: 6000\n\u2502   \u251c\u2500\u2500 MONITORING_PORT: 6001\n\u2502   \u251c\u2500\u2500 SERVICE_PORT: 6002\n\u2502   \u251c\u2500\u2500 METRICS_PORT: 6003\n\u2502   \u251c\u2500\u2500 DELAY: 1\n\u2502   \u2514\u2500\u2500 STORAGE_MNEMONIC: CM\n\u2514\u2500\u2500 Storage Control Server\n    \u251c\u2500\u2500 PROTOCOL: tcp\n    \u251c\u2500\u2500 HOSTNAME: localhost\n    \u251c\u2500\u2500 COMMANDING_PORT: 6100\n    \u251c\u2500\u2500 MONITORING_PORT: 6101\n    \u251c\u2500\u2500 SERVICE_PORT: 6102\n    \u251c\u2500\u2500 METRICS_PORT: 6103\n    \u2514\u2500\u2500 DELAY: 1\n

If you only need the settings for a particular component, specify that group's name:

>>> storage_settings = Settings.load(\"Storage Control Server\")\n\n>>> print(storage_settings)\nStorage\nControl\nServer\n\u251c\u2500\u2500 PROTOCOL: tcp\n\u251c\u2500\u2500 HOSTNAME: localhost\n\u251c\u2500\u2500 COMMANDING_PORT: 6100\n\u251c\u2500\u2500 MONITORING_PORT: 6101\n\u251c\u2500\u2500 SERVICE_PORT: 6102\n\u251c\u2500\u2500 METRICS_PORT: 6103\n\u2514\u2500\u2500 DELAY: 1\n

The values can be accessed as usual with a dictionary, by specifying the name of the parameter as the key:

>>> print(storage_settings[\"COMMANDING_PORT\"])\n6100\n

We usually only go one level deep when defining settings, and as a convenience, that first level of variables can also be accessed with the dot-notation.

>>> print(storage_settings.COMMANDING_PORT)\n6100\n
"},{"location":"libs/cgse-common/settings/#entry-points","title":"Entry-points","text":"

The Settings are collected from a set of YAML files which are provided by the packages through the entry-point cgse.settings. The default Settings file is named settings.yaml but this can be changed by the entry-point (see below).

Let's take a look at how the settings are provided for the cgse-core package. First, the pyproject.toml file of the project shall define the entry-point. In the snippet below, the entry-point cgse-core is defined for the group cgse.settings.

[project.entry-points.\"cgse.settings\"]\ncgse-core = \"cgse_core:settings.yaml\"\n

The entry-point itself has the following format: <name> = \"<module>.<filename>\", where

Note

The module name for this entry point has an underscore instead of a dash, i.e. cgse_core instead of cgse-core. The reason is that module names with a dash will generate a SyntaxError during import.

The above example will load the settings for this package from the settings.yaml file that is located in the cgse_core module. That is, the package shall also provide this as follows:

cgse-core\n\u251c\u2500\u2500 pyproject.toml\n\u2514\u2500\u2500 src\n    \u2514\u2500\u2500 cgse_core\n        \u251c\u2500\u2500 __init__.py\n        \u2514\u2500\u2500 settings.yaml\n

The settigs.yaml file for this module looks something like this:

PACKAGES:\n    CGSE_CORE: The core services of the CGSE\n\nLogging Control Server:                          # LOG_CS\n\n    PROTOCOL:                       tcp\n    HOSTNAME:                 localhost          # The hostname that client shall connect to, e.g. pleiad01 @ KU Leuven\n    LOGGING_PORT:                  7000\n    COMMANDING_PORT:               7001\n    METRICS_PORT:                  7003          # The HTTP port where Prometheus will connect to for retrieving metrics\n    MAX_NR_LOG_FILES:                20          # The maximum number of log files that will be maintained in a roll-over\n    MAX_SIZE_LOG_FILES:              20          # The maximum size one log file can become\n    TEXTUALOG_IP_ADDRESS:     127.0.0.1          # The IP address of the textualog listening server\n    TEXTUALOG_LISTENING_PORT:     19996          # The port number on which the textualog server is listening\n\nConfiguration Manager Control Server:            # CM_CS\n\n    ...\n

Warning

Please note that the module where the Settings YAML file resides is a Python package and not a namespace. That means it shall have a __init__.py file as shown in the example of the cgse_core module above.

If the __init__.py file is not there, you will get an error like below:

ERROR:egse.plugin:The entry-point 'cgse-coordinates' is ill defined. The module part doesn't \nexist or is a namespace. No settings are loaded for this entry-point.\n
"},{"location":"libs/cgse-common/settings/#local-settings","title":"Local Settings","text":"

You can, and you should, define local settings for your project and put those settings in a known folder on your system. The usual place is ~/cgse/local-settings.yaml. This file will be automatically loaded by the Settings.load() function when you define the local settings environment variable. That variable name is <PROJECT>_LOCAL_SETTINGS where <PROJECT> is the name of your project as defined by the PROJECT environment variable. For a PROJECT=LAB23 the local settings would be defined as follows:

$ export LAB23_LOCAL_SETTINGS=~/cgse/local-settings-lab23.yaml\n

The local settings take higher precedence that will overwrite the global settings when loaded. You only need to define the settings that actually change for your local installation, respect the full hierarchy when specifying those settings. You are allowed to define new entries at any level in the Settings hierarchy.

The usual parameters to put into a local settings file are:

"},{"location":"libs/cgse-common/settings/#terminal-command","title":"Terminal Command","text":"

You can check the current settings from the terminal with the following command:

$ py -m egse.settings\nSettings\n\u251c\u2500\u2500 PACKAGES\n\u2502   \u251c\u2500\u2500 CGSE_COMMON: Common classes, functions, decorators, etc. for the CGSE\n\u2502   \u2514\u2500\u2500 CGSE_CORE: The core services of the CGSE\n\u251c\u2500\u2500 SITE\n\u2502   \u251c\u2500\u2500 ID: LAB42\n\u2502   \u251c\u2500\u2500 SSH_SERVER: localhost\n\u2502   \u2514\u2500\u2500 SSH_PORT: 22\n\u251c\u2500\u2500 PROCESS\n\u2502   \u2514\u2500\u2500 METRICS_INTERVAL: 10\n\u251c\u2500\u2500 Logging Control Server\n\u2502   \u251c\u2500\u2500 PROTOCOL: tcp\n\u2502   \u251c\u2500\u2500 HOSTNAME: localhost\n\u2502   \u251c\u2500\u2500 LOGGING_PORT: 7000\n\u2502   \u251c\u2500\u2500 COMMANDING_PORT: 7001\n... ...\n\u2514\u2500\u2500 Storage Control Server\n    \u251c\u2500\u2500 PROTOCOL: tcp\n    \u251c\u2500\u2500 HOSTNAME: localhost\n    \u251c\u2500\u2500 COMMANDING_PORT: 6100\n    \u251c\u2500\u2500 MONITORING_PORT: 6101\n    \u251c\u2500\u2500 SERVICE_PORT: 6102\n    \u251c\u2500\u2500 METRICS_PORT: 6103\n    \u2514\u2500\u2500 DELAY: 1\nMemoized locations:\n['/Users/rik/github/cgse/libs/cgse-common/src/cgse_common/settings.yaml', '/Users/rik/github/cgse/libs/cgse-core/src/cgse_core/settings.yaml', '/Users/rik/cgse/local_settings_ariel.yaml']\n

The memoized locations are the settings files that have been loaded and cached. Once the application has started and the settings have been loaded, they can only be reloaded by explicitly forcing a reload as follows:

>>> settings = Settings.load(force=True)\n

Warning

The force reload does however not guarantee that the settings will propagate properly throughout the application or to client apps. Settings can be saved in local variables or class instances that have no knowledge of a Settings reload. So, be careful when changing your Settings. If there are parameters that change often and are not as static as thought, maybe they belong in the Setup instead of the Settings. Examples are:

"},{"location":"libs/cgse-common/settings/#the-design-of-the-load-method","title":"The design of the load() method","text":"

A word about the Settings.load() method. Depending on the parameters provided, this method either loads all settings, a group of settings or just one single YAML file. We have already explained how to load a specific group of settings by giving the name of the group as a parameter. When you want to load just one YAML file, you need to specify its location also. When a location is given as a str or a Path, the Settings will be loaded from that file only, using the default settings.yaml name or another name given through the filename argument.

This can be used to e.g. load command files for a device:

>>> commands = Settings.load(location=\"~\", filename=\"DAQ5610.yaml\")\n

The mechanism behind the Settings.load() method is shown in the following diagram. For simplicity, parameters are not shown and only the success path is presented, not any exceptions or error handling.

"},{"location":"libs/cgse-common/setup/","title":"The Setup","text":""},{"location":"libs/cgse-coordinates/","title":"Reference Coordinates","text":""},{"location":"libs/cgse-core/","title":"Core Services","text":""},{"location":"libs/cgse-gui/","title":"GUI Components","text":""},{"location":"projects/","title":"Projects","text":"

The projects are those packages that add functionality to the CGSE framework.

The projects live under the folder projects, and they are organised in generic and specific projects. Generic projects do not have an implementation that is specific for one particular project, while, obviously, specific projects have. We currently have the following generic packages:

and then there are the project specific packages:

"},{"location":"projects/cgse-tools/","title":"Tools for the CGSE framework","text":""},{"location":"projects/symetrie-hexapod/","title":"The Sym\u00e9trie Hexapods","text":""},{"location":"projects/symetrie-hexapod/#settings-up-your-system-for-the-puna-hexapod","title":"Settings up your system for the PUNA Hexapod","text":"

Warning

We need some work here... we want to be able to use multiple hexapods in the same Setup and they can be the same type or different types. So, how do we specify two PUNA hexapods used to position two different parts of your test equiopment?

The system needs to know the following information on the hexapod:

These above settings can olso be specified in the environment variables:

"},{"location":"user_guide/","title":"User Guide","text":"

Welcome to the CGSE user guide! An in-depth reference on how to use the CGSE.

"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"

Tip

See the navigation links in the header or side-bar.

Click (top left) on mobile.

"},{"location":"#welcome","title":"Welcome","text":"

Welcome to the CGSE framework documentation.

Get started or go straight to the Tutorial

"},{"location":"#what-is-the-cgse","title":"What is the CGSE?","text":"

The CGSE is short for Common-EGSE, a framework for managing and running test equipment in a lab. The EGSE stands for Electrical Ground Support Equipment, and this includes all equipment that is used to test or calibration an instrument.

"},{"location":"getting_started/","title":"Getting started","text":"

All you need to get started using and building the CGSE.

"},{"location":"getting_started/#requirements","title":"Requirements","text":""},{"location":"getting_started/#virtual-environment","title":"Virtual environment","text":"

You should always work inside a virtual environment to somehow containerize your project such that it doesn't pollute your global environment and you can run different projects next to each other. Create and activate a new virtual environment as follows. It should be Python >= 3.9

$ python -m venv venv\n$ source venv/bin/activate\n
"},{"location":"getting_started/#installation","title":"Installation","text":"

The easiest way to install the CGSE is to use the pip command. Since the CGSE is a monorepo and consists of several packages, you will need to make your choice which package you need for your project. You can however start with the cgse-common which contains all common code that is generic and useful as a basis for other packages.

$ pip install cgse-common\n

Check the list of packages that are part of the CGSE repo and can be installed with pip. The packages are described in more detail in the sections Libs and Projects.

"},{"location":"getting_started/#set-up-your-environment","title":"Set up your environment","text":"

To check your installation and set up your environment, here are a few tips.

The version of the core packages and any plugin packages can be verified as follows. The version you installed will probably be higher and more lines will appear when other packages are installed.

$ py -m egse.version\nCGSE version in Settings: 0.5.0\nInstalled version for cgse-common= 0.5.0\n

Check your environment with the command below. This will probably print out some warning since you have not defined the expected environment variables yet. There are two mandatory environment variables: PROJECT and SITE_ID. The former shall contain the name of your project without spaces and preferably a single word or an acronym like PLATO, ARIEL, MARVEL, MERCATOR. The latter is the name of the site or lab where the tests will be performed. Good names are KUL, ESA, LAB23.

The other environment variables follow the pattern <PROJECT>_..., i.e. they all start with the project name as defined in the PROJECT environment variable. You should define at least <PROJECT>_DATA_STORAGE_LOCATION. The configuration data and log file location will be derived from it unless they are explicitly set themselves.

Let's define the three expected environment variables:

$ export PROJECT=ARIEL\n$ export SITE_ID=VACUUM_LAB\n$ export ARIEL_DATA_STORAGE_LOCATION=~/data\n

Rerunning the above command now gives:

$ py -m egse.env\nEnvironment variables:\n    PROJECT = ARIEL\n    SITE_ID = VACUUM_LAB\n    ARIEL_DATA_STORAGE_LOCATION = /Users/rik/data\n    ARIEL_CONF_DATA_LOCATION = not set\n    ARIEL_CONF_REPO_LOCATION = not set\n    ARIEL_LOG_FILE_LOCATION = not set\n    ARIEL_LOCAL_SETTINGS = not set\n\nGenerated locations and filenames\n    get_data_storage_location() = '/Users/rik/data/VACUUM_LAB'  \u27f6 ERROR: The data storage location doesn't exist!\n    get_conf_data_location() = '/Users/rik/data/VACUUM_LAB/conf'  \u27f6 ERROR: The configuration data location doesn't exist!\n    get_conf_repo_location() = None  \u27f6 ERROR: The configuration repository location doesn't exist!\n    get_log_file_location() = '/Users/rik/data/VACUUM_LAB/log'  \u27f6 ERROR: The log files location doesn't exist!\n    get_local_settings() = None  \u27f6 ERROR: The local settings file is not defined or doesn't exist!\n\nuse the '--full' flag to get a more detailed report, '--doc' for help on the variables.\n

Note

The folders that do not exist (and are not None) can be created by adding the option --mkdir to the above command.

"},{"location":"getting_started/#check-your-settings","title":"Check your Settings","text":"

Settings contains the static configuration of your system, test setup, equipment, including the System Under Test (SUT). You can test your settings with the command below. Let's first also set the ARIEL_LOCALSETTINGS environment variables:

$ export ARIEL_LOCAL_SETTINGS=~/cgse/local_settings_ariel_vacuum_lab.yaml\n$ py -m egse.settings\nSettings\n\u251c\u2500\u2500 PACKAGES\n\u2502   \u2514\u2500\u2500 CGSE_COMMON: Common classes, functions, decorators, etc. for the CGSE\n\u251c\u2500\u2500 SITE\n\u2502   \u251c\u2500\u2500 ID: VACUUM_LAB\n\u2502   \u251c\u2500\u2500 SSH_SERVER: localhost\n\u2502   \u2514\u2500\u2500 SSH_PORT: 22\n\u2514\u2500\u2500 PROCESS\n    \u2514\u2500\u2500 METRICS_INTERVAL: 10\nMemoized locations:\n['/Users/rik/tmp/gettings-started/venv/lib/python3.9/site-packages/cgse_common/settings.yaml', \n'/Users/rik/cgse/local_settings_ariel_vacuum_lab.yaml']\n
These Settings will grow when you add more packages to your installation or when you define settings yourself in the local settings file.

"},{"location":"help/","title":"Help","text":"

The best way to get help for something that you couldn't find in the documentation on this site, is to contact one of the authors.

"},{"location":"help/#bugs-and-feature-requests","title":"Bugs and Feature requests","text":"

Report any bugs or issues through GitHub on the CGSE issues page.

"},{"location":"initialize/","title":"Initialize your project","text":"

So, we have seen how to get started with some basic commands and only the cgse-common package. It's time now to initialize your project properly with all the necessary services.

"},{"location":"initialize/#set-up-your-environment","title":"Set up your environment","text":"

I assume you are in the same environment that we have set up in the previous section where also the cgse-common package was installed. We will install another package that will provide us with the full functionality of the cgse command. Install the cgse-tools and cgse-core packages which depends on cgse-core and will also install that package.

$ pip install cgse-tools cgse-core\n

You should now have at least the follow three packages installed in your virtual environment:

$ pip list | grep cgse\ncgse-common       0.5.0\ncgse-core         0.5.0\ncgse-tools        0.5.0\n
"},{"location":"initialize/#the-cgse-command","title":"The cgse command","text":"

The two new packages that have been installed (cgse-core and cgse-tools) provide the cgse command that we will use to initialize your environment, but this command is also used to inspect different parts of the system, manage core services and device drivers, etc.

When you run the cgse command without any arguments, it will show something like this:

$ cgse\n\n Usage: cgse [OPTIONS] COMMAND [ARGS]...\n\n The main cgse command to inspect, configure, monitor the core services and device control servers.\n\n\u256d\u2500 Options \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 --install-completion          Install completion for the current shell.                                                                                      \u2502\n\u2502 --show-completion             Show completion for the current shell, to copy it or customize the installation.                                               \u2502\n\u2502 --help                        Show this message and exit.                                                                                                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\u256d\u2500 Commands \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 version   Prints the version of the cgse-core and other registered packages.                                                                                 \u2502\n\u2502 init      Initialize your project.                                                                                                                           \u2502\n\u2502 top       A top-like interface for core services and device control servers.                                                                                 \u2502\n\u2502 core      handle core services: start, stop, status                                                                                                          \u2502\n\u2502 show      Show information about settings, environment, setup, ...                                                                                           \u2502\n\u2502 check     Check installation, settings, required files, etc.                                                                                                 \u2502\n\u2502 dev-x     device-x is an imaginary device that serves as an example                                                                                          \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n

The cgse command is actually an app that is the starting point for a number of commands that can be used to maintain the system, manage and inspect services and devices. For example, to check the version of the different components, use:

$ cgse version\nCGSE-COMMON installed version = 0.5.0 \u2014 Software framework to support hardware testing\nCGSE-CORE installed version = 0.5.0 \u2014 Core services for the CGSE framework\nCGSE-TOOLS installed version = 0.5.0 \u2014 Tools for CGSE\n

We will for now concentrate on the init command. This command will guide us through a number of steps to define the location of our device data, configuration data, etc. We will basically define some environment variables that are used by the CGSE framework. The PROJECT is he name of the project your will be working on, the SITE_ID is the identifier for the LAB or Setup that you are using to perform the tests. As you see below, the environment variables all start with the project name allowing you to work on different projects simultaneously. If you accept all the defaults, the result of the cgse init command will look something like this:

$ cgse init --project marvel\nPlease note default values are given between [brackets].\nWhat is the name of the project [MARVEL] ?:\nWhat is the site identifier ?: lab02\nWhere can the project data be stored [~/data/MARVEL/LAB02/] ?:\nWhere will the configuration data be located [~/data/MARVEL/LAB02/conf/] ?:\nWhere will the logging messages be stored [~/data/MARVEL/LAB02/log/] ?:\nWhere shall I create a local settings YAML file [~/data/MARVEL/LAB02/local_settings.yaml] ?:\nShall I add the environment to your ~/bash_profile ? [y/n]: n\n\n# -> Add the following lines to your bash profile or equivalent\n\nexport PROJECT=MARVEL\nexport SITE_ID=LAB02\nexport MARVEL_DATA_STORAGE_LOCATION=~/data/MARVEL/LAB02/\nexport MARVEL_CONF_DATA_LOCATION=~/data/MARVEL/LAB02/conf/\nexport MARVEL_LOG_FILE_LOCATION=~/data/MARVEL/LAB02/log/\nexport MARVEL_LOCAL_SETTINGS=~/data/MARVEL/LAB02/local_settings.yaml\n

If you answered 'Y' to the last question, you should log in to the shell again with exec bash -login or a similar command for other shells, or you should start a new terminal to activate the environment variables.

Add this point you are ready to go and start the core services and any device control servers that you need. You can explore other commands of the cgse app in the user guide.

"},{"location":"package_list/","title":"Packages in the CGSE","text":"

The CGSE is a monorepo and consists of numerous packages. Each of these packages are individually installable from PyPI. We maintain a list here with all the packages in the monorepo.

Package Description cgse-common The common code used by all other packages cgse-core The core services cgse-coordinates Coordinate reference Frames cgse-gui GUI components and styles (PyQt5) cgse-tools Plugin that adds functions to the cgse command symetrie-hexapod Device drivers for the Sym\u00e9trie Hexapods keithley-tempcontrol Device driver for the Keithley temperature controller plato-fits FITS driver with PLATO specific format plato-hdf5 HDF5 driver with PLATO specific format plato-spw SpaceWire driver with PATO specific packets

The following is a non-exhaustive list of known external packages that work well with the CGSE in terms of commanding and monitoring.

Package Description cgse-dummy Provides a dummy device driver to demonstrate plugins, commands and how to develop an external package for the CGSE."},{"location":"roadmap/","title":"Roadmap","text":"

Don't worry, the feature set will grow..

"},{"location":"roadmap/#features","title":"Features","text":""},{"location":"roadmap/#the-cgse-command","title":"The cgse Command","text":"

Provide a cgse command that is extensible with new commands and command groups:

"},{"location":"roadmap/#settings-setup-and-the-environment","title":"Settings, Setup and the environment","text":""},{"location":"roadmap/#common-functionality","title":"Common functionality","text":""},{"location":"roadmap/#devices","title":"Devices","text":""},{"location":"roadmap/#projects","title":"Projects","text":""},{"location":"roadmap/#guis-and-tuis","title":"GUIs and TUIs","text":""},{"location":"roadmap/#removals","title":"Removals","text":""},{"location":"roadmap/#testing","title":"Testing","text":""},{"location":"tutorial/","title":"Tutorial","text":"

Welcome to the CGSE Tutorial!

By the end of this page you should have a solid understanding of the core features of the CGSE.

"},{"location":"api/","title":"API","text":"

This is a API-level reference to the CGSE API. Click the links to your left (or in the menu) to open a reference for each module.

If you are new to the CGSE, you may want to read the Gettings Started first.

"},{"location":"api/bits/","title":"egse.bits","text":"

This module contains a number of convenience functions to work with bits, bytes and integers.

Functions:

Name Description beautify_binary

Returns a binary representation of the given value. The bits are presented

bit_set

Return True if the bit is set.

bits_set

Return True if all the bits are set.

clear_bit

Set bit to 0 for the given value.

clear_bits

Set the given bits in value to 0.

crc_calc

Calculate the checksum for (part of) the data.

extract_bits

Extracts a specified number of bits from an integer starting at a given position.

humanize_bytes

Represents the size n in human readable form, i.e. as byte, KiB, MiB, GiB, ...

s16

Return the signed equivalent of a hex or binary number.

s32

Return the signed equivalent of a hex or binary number.

set_bit

Set bit to 1 for the given value.

set_bits

Set the given bits in value to 1.

toggle_bit

Toggle the bit in the given value.

"},{"location":"api/bits/#egse.bits.beautify_binary","title":"beautify_binary","text":"
beautify_binary(value, sep=' ', group=8, prefix='', size=0)\n

Returns a binary representation of the given value. The bits are presented in groups of 8 bits for clarity by default (can be changed with the group keyword).

Parameters:

Name Type Description Default value int

the value to beautify

required sep str

the separator character to be used, default is a space

' ' group int

the number of bits to group together, default is 8

8 prefix str

a string to prefix the result, default is ''

'' size int

number of digits

0

Returns:

Type Description str

a binary string representation.

Examples:

>>> status = 2**14 + 2**7\n>>> assert beautify_binary(status) == \"01000000 10000000\"\n
"},{"location":"api/bits/#egse.bits.bit_set","title":"bit_set","text":"
bit_set(value, bit)\n

Return True if the bit is set.

Parameters:

Name Type Description Default value int

the value to check

required bit int

the index of the bit to check, starting from 0 at the LSB

required

Returns:

Type Description bool

True if the bit is set (1).

"},{"location":"api/bits/#egse.bits.bits_set","title":"bits_set","text":"
bits_set(value, *args)\n

Return True if all the bits are set.

Parameters:

Name Type Description Default value int

the value to check

required args

a set of indices of the bits to check, starting from 0 at the LSB

()

Returns:

Type Description bool

True if all the bits are set (1).

Examples:

>>> assert bits_set(0b0101_0000_1011, [0, 1, 3, 8, 10])\n>>> assert bits_set(0b0101_0000_1011, [3, 8])\n>>> assert not bits_set(0b0101_0000_1011, [1, 2, 3])\n
"},{"location":"api/bits/#egse.bits.clear_bit","title":"clear_bit","text":"
clear_bit(value, bit)\n

Set bit to 0 for the given value.

Parameters:

Name Type Description Default value int

the integer value that needs a bit set or unset

required bit int

the index of the bit to set/unset, starting from 0 at the LSB

required

Returns:

Type Description int

the changed value.

"},{"location":"api/bits/#egse.bits.clear_bits","title":"clear_bits","text":"
clear_bits(value, bits)\n

Set the given bits in value to 0.

Parameters:

Name Type Description Default value int

the value where the given bits shall be changed

required bits tuple

a tuple with start and stop bits

required

Returns:

Type Description int

the changed value

"},{"location":"api/bits/#egse.bits.crc_calc","title":"crc_calc","text":"
crc_calc(data, start, len)\n

Calculate the checksum for (part of) the data.

Parameters:

Name Type Description Default data

the data for which the checksum needs to be calculated

required start int

offset into the data array [byte]

required len int

number of bytes to incorporate into the calculation

required

Returns:

Type Description int

the calculated checksum.

Reference

The description of the CRC calculation for RMAP is given in the ECSS document Space Engineering: SpaceWire - Remote Memory Access Protocol, section A.3 on page 80 [ECSS\u2010E\u2010ST\u201050\u201052C].

"},{"location":"api/bits/#egse.bits.extract_bits","title":"extract_bits","text":"
extract_bits(value, start_position, num_bits)\n

Extracts a specified number of bits from an integer starting at a given position.

Parameters:

Name Type Description Default value int

The input integer.

required start_position int

The starting bit position (0-based index).

required num_bits int

The number of bits to extract.

required

Returns:

Name Type Description int int

The extracted bits as an integer.

"},{"location":"api/bits/#egse.bits.humanize_bytes","title":"humanize_bytes","text":"
humanize_bytes(n, base=2, precision=3)\n

Represents the size n in human readable form, i.e. as byte, KiB, MiB, GiB, ...

Parameters:

Name Type Description Default n int

number of byte

required base (int, str)

binary (2) or decimal (10)

2 precision int

the number of decimal places [default=3]

3

Returns:

Type Description str

a human readable size, like 512 byte or 2.300 TiB

Raises:

Type Description ValueError

when base is different from 2 (binary) or 10 (decimal).

Examples:

>>> assert humanize_bytes(55) == \"55 bytes\"\n>>> assert humanize_bytes(1024) == \"1.000 KiB\"\n>>> assert humanize_bytes(1000, base=10) == \"1.000 kB\"\n>>> assert humanize_bytes(1000000000) == '953.674 MiB'\n>>> assert humanize_bytes(1000000000, base=10) == '1.000 GB'\n>>> assert humanize_bytes(1073741824) == '1.000 GiB'\n>>> assert humanize_bytes(1024**5 - 1, precision=0) == '1024 TiB'\n
Note

Please note that, by default, I use the IEC standard (International Engineering Consortium) which is in base=2 (binary), i.e. 1024 bytes = 1.0 KiB. If you need SI units (International System of Units), you need to specify base=10 (decimal), i.e. 1000 bytes = 1.0 kB.

"},{"location":"api/bits/#egse.bits.s16","title":"s16","text":"
s16(value)\n

Return the signed equivalent of a hex or binary number.

Parameters:

Name Type Description Default value int

an integer value.

required

Returns:

Type Description int

The negative equivalent of a twos-complement binary number.

Examples:

Since integers in Python are objects and stored in a variable number of bits, Python doesn't know the concept of twos-complement for negative integers. For example, this 16-bit number

>>> 0b1000_0000_0001_0001\n32785\n

which in twos-complement is actually a negative value:

>>> s16(0b1000_0000_0001_0001)\n-32751\n

The 'bin()' fuction will return a strange representation of this number:

>>> bin(-32751)\n'-0b111111111101111'\n

when we however mask the value we get:

>>> bin(-32751 & 0b1111_1111_1111_1111)\n'0b1000000000010001'\n
See

Twos complement in Python and Pythons representation of negative integers and Signed equivalent of a twos-complement hex-value and SO Twos complement in Python.

"},{"location":"api/bits/#egse.bits.s32","title":"s32","text":"
s32(value)\n

Return the signed equivalent of a hex or binary number.

Parameters:

Name Type Description Default value int

an integer value.

required

Returns:

Type Description

The negative equivalent of a twos-complement binary number.

Examples:

Since integers in Python are objects and stored in a variable number of bits, Python doesn't know the concept of twos-complement for negative integers. For example, this 32-bit number

>>> 0b1000_0000_0000_0000_0000_0000_0001_0001\n2147483665\n

which in twos-complement is actually a negative value:

>>> s32(0b1000_0000_0000_0000_0000_0000_0001_0001)\n-2147483631\n
"},{"location":"api/bits/#egse.bits.set_bit","title":"set_bit","text":"
set_bit(value, bit)\n

Set bit to 1 for the given value.

Parameters:

Name Type Description Default value int

the integer value that needs a bit set or unset

required bit int

the index of the bit to set/unset, starting from 0 at the LSB

required

Returns:

Type Description int

the changed value.

"},{"location":"api/bits/#egse.bits.set_bits","title":"set_bits","text":"
set_bits(value, bits)\n

Set the given bits in value to 1.

Parameters:

Name Type Description Default value int

the value where the given bits shall be changed

required bits tuple

a tuple with start and stop bits

required

Returns:

Type Description int

the changed value.

"},{"location":"api/bits/#egse.bits.toggle_bit","title":"toggle_bit","text":"
toggle_bit(value, bit)\n

Toggle the bit in the given value.

Parameters:

Name Type Description Default value int

the integer value that needs a bit toggled

required bit int

the index of the bit to toggle, starting from 0 at the LSB

required

Returns:

Type Description int

the changed value.

"},{"location":"api/calibration/","title":"egse.calibration","text":"

This module provides functions to calibrate sensor values.

Functions:

Name Description apply_gain_offset

Applies the given gain and offset to the given counts.

callendar_van_dusen

Solves the Callendar - van Dusen equation for temperature.

chebychev

Solves the Chebychev equation for temperature.

counts_to_resistance

Converts the given counts for the given sensor to resistance.

counts_to_temperature

Converts the given counts for the given sensor to temperature.

resistance_to_temperature

Converts the given resistance for the given sensor to temperature.

solve_temperature

Solves the temperature from the temperature -> resistance polynomial.

"},{"location":"api/calibration/#egse.calibration.apply_gain_offset","title":"apply_gain_offset","text":"
apply_gain_offset(counts, gain, offset)\n

Applies the given gain and offset to the given counts.

Parameters:

Name Type Description Default counts float

Uncalibrated, raw data [ADU]

required gain float

Gain to apply

required offset float

Offset to apply

required

Returns:

Type Description float

Counts after applying the given gain and offset.

"},{"location":"api/calibration/#egse.calibration.callendar_van_dusen","title":"callendar_van_dusen","text":"
callendar_van_dusen(\n    resistance, ref_resistance, standard, setup\n)\n

Solves the Callendar - van Dusen equation for temperature.

Parameters:

Name Type Description Default resistance float

Resistance [Ohm] for which to calculate the temperature

required ref_resistance float

Resistance [Ohm] for a temperature of 0\u00b0C

required standard str

Sensor standard

required setup Setup

Setup

required

Returns:

Type Description float

Temperature [\u00b0C] corresponding to the given resistance.

"},{"location":"api/calibration/#egse.calibration.chebychev","title":"chebychev","text":"
chebychev(resistance, sensor_info)\n

Solves the Chebychev equation for temperature.

Implemented as specified in the calibration certificate of the LakeShore Cernox sensors.

Parameters:

Name Type Description Default resistance float

Resistance [Ohm] for which to calculate the temperature

required sensor_info navdict

Calibration information

required

Returns:

Type Description float

Temperature [\u00b0C] corresponding to the given resistance.

"},{"location":"api/calibration/#egse.calibration.counts_to_resistance","title":"counts_to_resistance","text":"
counts_to_resistance(sensor_name, counts, sensor_info)\n

Converts the given counts for the given sensor to resistance.

Parameters:

Name Type Description Default sensor_name str

Sensor name

required counts float

Uncalibrated, raw data [ADU]

required sensor_info NavigableDict

Calibration information for the given sensor (type)

required

Returns:

Type Description float

Resistance [Ohm] for the given sensor.

"},{"location":"api/calibration/#egse.calibration.counts_to_temperature","title":"counts_to_temperature","text":"
counts_to_temperature(\n    sensor_name, counts, sensor_info, setup\n)\n

Converts the given counts for the given sensor to temperature.

This conversion can be done as follows:

- (1) Directly from counts to temperature, by applying the gain and offset;\n- (2) Directly from counts to temperature, by applying a function;\n- (3) From counts, via resistance, to temperature.\n

Parameters:

Name Type Description Default sensor_name str

Sensor name

required counts float

Uncalibrated, raw data [ADU]

required sensor_info NavigableDict

Calibration information for the given sensor (type)

required setup Setup

Setup

required

Returns:

Type Description float

Calibrated temperature [\u00b0C] for the given sensor

"},{"location":"api/calibration/#egse.calibration.resistance_to_temperature","title":"resistance_to_temperature","text":"
resistance_to_temperature(\n    sensor_name, resistance, sensor_info, setup\n)\n

Converts the given resistance for the given sensor to temperature.

Parameters:

Name Type Description Default sensor_name str

Sensor name

required resistance float

Resistance [Ohm]

required sensor_info NavigableDict

Calibration information for the given sensor (type)

required setup Setup

Setup

required

Returns:

Type Description floaf

Temperature [\u00b0C] for the given sensor.

"},{"location":"api/calibration/#egse.calibration.solve_temperature","title":"solve_temperature","text":"
solve_temperature(\n    temperature_to_resistance_coefficients, resistance\n)\n

Solves the temperature from the temperature -> resistance polynomial.

For the given temperature -> resistance polynomial and the given resistance, we determine what the corresponding temperature is by:

"},{"location":"api/command/","title":"egse.command","text":"

This module defines a number of classes and helper functions to define and work with commands that operate hardware devices. The goal is to be able to define / create commands transparently from a YAML file without having to write (too much) code.

"},{"location":"api/command/#egse.command--definitions","title":"Definitions","text":"

command

a string that is sent to a device over an interface like TCP/IP or USB. This string is generated by the get_cmd_string() method of the Command class.

The string contains format like syntax that looks like an f-string, but is interpreted differently. See further: How to format device command strings.

Command

the base class for commands. This class contains the definition of the command and provides methods to parse and check arguments. The Command can be 'called' or 'executed' in which case a number of actions are performed based on the provided arguments.

CommandExecution

this class contains all the information needed to execute a command, without actually executing it. A CommandExecution contains the command definition and the parameters for the execution. It is mainly served as a communication mechanism to the control servers, i.e. the client side (Proxy) defines a command execution and the server then executes the command.

CommandError

a catch-all exception for unrecoverable errors in this module

InvalidArgumentsError

a CommandError raised when the arguments provided are themselve invalid or if the number of arguments is not matching expectations

The basic interface is:

cmd = Command(name     = <command name>,\n              cmd      = <command string>,\n              response = <callable to retreive a response>,\n              wait     = <callable to wait a specific time/delay>)\n

where:

"},{"location":"api/command/#egse.command--formatting-device-command-strings","title":"Formatting device command strings","text":"

The cmd argument is a string that contains placeholders (replacement fields) for future arguments that will be passed when calling the Command. The replacement fields are marked with curly braces and are mandatory. When a name is provided in the curly braces, the argument shall be provided as a keyword argument, otherwise a positional argument is expected. In the current implementation the cmd can only contain either positional arguments or keyword argument, not a mix of both.

The replacement fields may also have a format specifier to specify a precise format for that field.

Examples

moveAbsolute = Command(\n    name = \"moveAbsolute\",\n    cmd  = \"&2 Q70=0 Q71={tx:.6f} Q72={ty:.6f} Q73={tz:.6f} \"\n           \"Q74={rx:.6f} Q75={ry:.6f} Q76={rz:.6f} Q20=11\"\n)\n\nresponse = moveAbsolute(1, 1, 1, 0, 0, 20)\nresponse = moveAbsolute(tx=1, ty=1, tz=1, rx=0, ry=0, rz=20)\n
"},{"location":"api/command/#egse.command--questions","title":"Questions","text":"

Do we need additional hooks into this commanding?

Classes:

Name Description ClientServerCommand Command

A Command is basically a string that is send to a device and for which the

CommandError

A Command Exception as a base class for this module.

CommandExecution

This class contains all the information that is needed to execute a command

InvalidArgumentsError

The arguments provided are invalid

InvalidCommandExecution

A invalid command execution.

Functions:

Name Description dry_run

This decorator prepares the function to handle a dry run.

get_function

Returns a function (unbound method) from a given class.

get_method

Returns a bound method from a given class instance.

load_commands

Loads the command definitions from the given command_settings and builds an internal

parse_format_string

Parse and decode the format string.

"},{"location":"api/command/#egse.command.ClientServerCommand","title":"ClientServerCommand","text":"
ClientServerCommand(\n    name,\n    cmd,\n    response=None,\n    wait=None,\n    check=None,\n    description=None,\n    device_method=None,\n)\n

Bases: Command

Methods:

Name Description client_call

This method is called at the client side. It is used by the Proxy

server_call

This method is called at the server side. It is used by the CommandProtocol class in the

"},{"location":"api/command/#egse.command.ClientServerCommand.client_call","title":"client_call","text":"
client_call(other, *args, **kwargs)\n

This method is called at the client side. It is used by the Proxy as a generic command to send a command execution to the server.

Parameters:

Name Type Description Default other

a sub-class of the Proxy class

required args

arguments that will be passed on to this command when executed

() kwargs

keyword arguments that will be passed on to this command when executed

{}

Returns:

Type Description

the response that is returned by calling the command (at the server side).

"},{"location":"api/command/#egse.command.ClientServerCommand.server_call","title":"server_call","text":"
server_call(other, *args, **kwargs)\n

This method is called at the server side. It is used by the CommandProtocol class in the execute method.

Parameters:

Name Type Description Default other

a sub-class of the CommandProtocol

required args

arguments are passed on to the response method

() kwargs

keyword arguments are passed on to the response method

{}

Returns:

Type Description

0 on success and -1 on failure.

"},{"location":"api/command/#egse.command.Command","title":"Command","text":"
Command(\n    name,\n    cmd,\n    response=None,\n    wait=None,\n    check=None,\n    description=None,\n    device_method=None,\n)\n

A Command is basically a string that is send to a device and for which the device returns a response.

The command string can contain placeholders that will be filled when the command is 'called'.

The arguments that are given will be filled into the formatted string. Arguments can be positional or keyword arguments, not both.

"},{"location":"api/command/#egse.command.CommandError","title":"CommandError","text":"

Bases: Error

A Command Exception as a base class for this module.

"},{"location":"api/command/#egse.command.CommandExecution","title":"CommandExecution","text":"
CommandExecution(cmd, *args, **kwargs)\n

This class contains all the information that is needed to execute a command with a set of parameters/arguments. The command is however not executed automatically. That is the responsibility of the caller to actually execute the command with the given parameters.

Developer info

you can see this as a partial (functools) which defines the command and its arguments, but doesn't execute until explicitly called. You can execute the command by calling the cmd with the given arguments:

ce = CommandExecution(cmd, 20.0)\n...\nresponse = ce.run()\n
"},{"location":"api/command/#egse.command.InvalidArgumentsError","title":"InvalidArgumentsError","text":"

Bases: CommandError

The arguments provided are invalid

"},{"location":"api/command/#egse.command.InvalidCommandExecution","title":"InvalidCommandExecution","text":"
InvalidCommandExecution(exc, cmd, *args, **kwargs)\n

Bases: CommandExecution

A invalid command execution.

Parameters:

Name Type Description Default exc

the Exception that was raised and describes the problem

required cmd

the Command object

required *args

the positional arguments that were given

() **kwargs

the keyword arguments that were given

{}"},{"location":"api/command/#egse.command.dry_run","title":"dry_run","text":"
dry_run(func)\n

This decorator prepares the function to handle a dry run.

A dry run is used to check the logic of an instrument commanding script without actually executing the instrument commands. The commands are instead added to the command sequence in the global state.

Parameters:

Name Type Description Default func Callable

the function that needs to be executed

required

Returns:

Type Description Callable

A wrapper around the given function.

"},{"location":"api/command/#egse.command.get_function","title":"get_function","text":"
get_function(parent_class, method_name)\n

Returns a function (unbound method) from a given class.

Parameters:

Name Type Description Default parent_class

the class that provides the method

required method_name str

name of the method that is requested

required

Returns:

Type Description Callable

the method [type: function].

Note

The function returned is an unbound class instance method and therefore this function expects as its first argument the class instance, i.e. self, when you call it as a function.

"},{"location":"api/command/#egse.command.get_method","title":"get_method","text":"
get_method(parent_obj, method_name)\n

Returns a bound method from a given class instance.

Parameters:

Name Type Description Default parent_obj

the class instance that provides the method

required method_name str

name of the method that is requested

required

Returns:

Type Description Callable

the method [type: method].

Note

The method returned is an bound class instance method and therefore this method does not expects as its first argument the class instance, i.e. self, when you call this as a function.

"},{"location":"api/command/#egse.command.load_commands","title":"load_commands","text":"
load_commands(\n    protocol_class,\n    command_settings,\n    command_class,\n    device_class,\n)\n

Loads the command definitions from the given command_settings and builds an internal dictionary containing the command names as keys and the corresponding Command class objects as values.

The command_settings is usually loaded from a YAML configuration file containing the command definitions for the device.

Parameters:

Name Type Description Default protocol_class

the CommandProtocol or a sub-class

required command_settings

a dictionary containing the command definitions for this device

required command_class

the type of command to create, a subclass of Command

required device_class

the type of the base device class from which the methods are loaded

required"},{"location":"api/command/#egse.command.parse_format_string","title":"parse_format_string","text":"
parse_format_string(fstring)\n

Parse and decode the format string.

"},{"location":"api/config/","title":"egse.config","text":"

This module provides convenience functions to properly configure the CGSE and to find paths and resources.

Classes:

Name Description WorkingDirectory

WorkingDirectory is a context manager to temporarily change the working directory while

Functions:

Name Description find_dir

Find the first folder that matches the given pattern.

find_dirs

Generator for returning directory paths from a walk started at root and matching pattern.

find_file

Find the path to the given file starting from the root directory of the

find_files

Generator for returning file paths from a top folder, matching the pattern.

find_first_occurrence_of_dir

Returns the full path of the directory that first matches the pattern. The directory hierarchy is

find_root

Find the root folder based on the files in tests.

get_common_egse_root

Returns the absolute path to the installation directory for the Common-EGSE.

get_resource_dirs

Define directories that contain resources like images, icons, and data files.

get_resource_path

Searches for a data file (resource) with the given name.

set_logger_levels

Set the logging level for the given loggers.

"},{"location":"api/config/#egse.config.WorkingDirectory","title":"WorkingDirectory","text":"
WorkingDirectory(path)\n

WorkingDirectory is a context manager to temporarily change the working directory while executing some code.

This context manager has a property path which returns the absolute path of the current directory.

Parameters:

Name Type Description Default path (str, Path)

the folder to change to within this context

required

Raises:

Type Description ValueError

when the given path doesn't exist.

Example
with WorkingDirectory(find_dir(\"/egse/images\")) as wdir:\n    for file in wdir.path.glob('*'):\n        assert file.exists()  # do something with the image files\n

Attributes:

Name Type Description path

Resolve and return the current Path of the context.

"},{"location":"api/config/#egse.config.WorkingDirectory.path","title":"path property","text":"
path\n

Resolve and return the current Path of the context.

"},{"location":"api/config/#egse.config.find_dir","title":"find_dir","text":"
find_dir(pattern, root=None)\n

Find the first folder that matches the given pattern.

Note that if there are more folders that match the pattern in the distribution, this function only returns the first occurrence that is found, which might not be what you want. To be sure only one folder is returned, use the find_dirs() function and check if there is just one item returned in the list.

Parameters:

Name Type Description Default pattern str

pattern to match (use * for wildcard)

required root str

the top level folder to search [default=common-egse-root]

None

Returns:

Type Description Path | None

the first occurrence of the directory pattern or None when not found.

"},{"location":"api/config/#egse.config.find_dirs","title":"find_dirs","text":"
find_dirs(pattern, root=None)\n

Generator for returning directory paths from a walk started at root and matching pattern.

The pattern can contain the asterisk '*' as a wildcard.

The pattern can contain a directory separator '/' which means the last part of the path needs to match these folders.

Parameters:

Name Type Description Default pattern str

pattern to match (use * for wildcard)

required root str

the top level folder to search [default=common-egse-root]

None

Returns:

Name Type Description Generator None

Paths of folders matching pattern, from root.

Example
>>> for folder in find_dirs(\"/egse/images\"):\n...     assert folder.match('*/egse/images')\n\n>>> folders = list(find_dirs(\"/egse/images\"))\n>>> assert len(folders)\n
"},{"location":"api/config/#egse.config.find_file","title":"find_file","text":"
find_file(name, root=None, in_dir=None)\n

Find the path to the given file starting from the root directory of the distribution.

Note that if there are more files with the given name found in the distribution, this function only returns the first file that is found, which might not be what you want. To be sure only one file is returned, use the find_files() function and check if there is just one file returned in the list.

When the file shall be in a specific directory, use the in_dir keyword. This requires that the path ends with the given string in in_dir.

>>> file_pattern = 'EtherSpaceLink*.dylib'\n>>> in_dir = 'lib/CentOS-7'\n>>> file = find_file(file_pattern, in_dir=in_dir)\n>>> assert file.match(\"*/lib/CentOS-7/EtherSpace*\")\n

Parameters:

Name Type Description Default name str

the name of the file

required root str

the top level folder to search [default=common-egse-root]

None in_dir str

the 'leaf' directory in which the file shall be

None

Returns:

Type Description Path | None

the first occurrence of the file or None when not found.

"},{"location":"api/config/#egse.config.find_files","title":"find_files","text":"
find_files(pattern, root=None, in_dir=None)\n

Generator for returning file paths from a top folder, matching the pattern.

The top folder can be specified as e.g. __file__ in which case the parent of that file will be used as the top root folder. Note that when you specify '.' as the root argument the current working directory will be taken as the root folder, which is probably not what you intended.

When the file shall be in a specific directory, use the in_dir keyword. This requires that the path ends with the given string in in_dir.

>>> file_pattern = 'EtherSpaceLink*.dylib'\n>>> in_dir = 'lib/CentOS-7'\n>>> for file in find_files(file_pattern, in_dir=in_dir):\n...     assert file.match(\"*lib/CentOS-7/EtherSpaceLink*\")\n

Parameters:

Name Type Description Default pattern str)

sorting pattern (use * for wildcard)

required root str

the top level folder to search [default=common-egse-root]

None in_dir str

the 'leaf' directory in which the file shall be

None

Returns:

Type Description

Paths of files matching pattern, from root.

"},{"location":"api/config/#egse.config.find_first_occurrence_of_dir","title":"find_first_occurrence_of_dir","text":"
find_first_occurrence_of_dir(pattern, root=None)\n

Returns the full path of the directory that first matches the pattern. The directory hierarchy is traversed in alphabetical order. The pattern is matched first against all directories in the root folder, if there is no match, the first folder in root is traversed until a match is found. If no match is found, the second folder in root is traversed.

Note that the pattern may contain parent directories, like /egse/data/icons or egse/*/icons, in which case the full pattern is matched.

Parameters:

Name Type Description Default pattern str

a filename pattern

required root Path | str

the root folder to start the hierarchical search

None

Returns:

Type Description Path | None

The full path of the matched pattern or None if no match could be found.

"},{"location":"api/config/#egse.config.find_root","title":"find_root","text":"
find_root(path, tests=(), default=None)\n

Find the root folder based on the files in tests.

The algorithm crawls backward over the directory structure until one of the items in tests is matched. and it will return that directory as a Path.

When no root folder can be determined, the default parameter is returned as a Path (or None).

When nothing is provided in tests, all matches will fail and the default parameter will be returned.

Parameters:

Name Type Description Default path Union[str, PurePath] | None

folder from which the search is started

required tests Tuple[str, ...]

names (files or dirs) to test for existence

() default str

returned when no root is found

None

Returns:

Type Description Union[PurePath, None]

a Path which is the root folder.

"},{"location":"api/config/#egse.config.get_common_egse_root","title":"get_common_egse_root cached","text":"
get_common_egse_root(path=None)\n

Returns the absolute path to the installation directory for the Common-EGSE.

The algorithm first tries to determine the path from the environment variable PLATO_COMMON_EGSE_PATH. If this environment variable doesn't exist, the algorithm tries to determine the path automatically from (1) the git root if it is a git repository, or (2) from the location of this module assuming the installation is done from the GitHub distribution.

When the optional argument path is given, that directory will be used to start the search for the root folder.

At this moment the algorithm does not cache the egse_path in order to speed up the successive calls to this function.

Parameters:

Name Type Description Default path str or Path

a directory as a Path or str [optional]

None

Returns:

Name Type Description Path Optional[PurePath]

the absolute path to the Common-EGSE installation directory or None

"},{"location":"api/config/#egse.config.get_resource_dirs","title":"get_resource_dirs","text":"
get_resource_dirs(root_dir=None)\n

Define directories that contain resources like images, icons, and data files.

Resource directories can have the following names: resources, data, icons, or images. This function checks if any of the resource directories exist in the project root directory, in the root_dir that is given as an argument or in the src/egse sub-folder.

So, the directories that are searched for the resource folders are:

For all existing directories the function returns the absolute path.

Parameters:

Name Type Description Default root_dir str

the directory to search for resource folders

None

Returns:

Type Description List[Path]

a list of absolute Paths.

"},{"location":"api/config/#egse.config.get_resource_path","title":"get_resource_path","text":"
get_resource_path(name, resource_root_dir=None)\n

Searches for a data file (resource) with the given name.

When resource_root_dir is not given, the search for resources will start at the root folder of the project (using the function get_common_egse_root()). Any other root directory can be given, e.g. if you want to start the search from the location of your source code file, use Path(__file__).parent as the resource_root_dir argument.

Parameters:

Name Type Description Default name str

the name of the resource that is requested

required resource_root_dir str

the root directory where the search for resources should be started

None

Returns:

Type Description PurePath

the absolute path of the data file with the given name. The first name that matches is returned. If no file with the given name or path exists, a FileNotFoundError is raised.

"},{"location":"api/config/#egse.config.set_logger_levels","title":"set_logger_levels","text":"
set_logger_levels(logger_levels=None)\n

Set the logging level for the given loggers.

"},{"location":"api/control/","title":"egse.control","text":"

This module defines the abstract class for any Control Server and some convenience functions.

Classes:

Name Description ControlServer

Base class for all device control servers and for the Storage Manager and Configuration Manager.

Functions:

Name Description is_control_server_active

Checks if the Control Server is running.

"},{"location":"api/control/#egse.control.ControlServer","title":"ControlServer","text":"
ControlServer()\n

Base class for all device control servers and for the Storage Manager and Configuration Manager.

A Control Server reads commands from a ZeroMQ socket and executes these commands by calling the execute() method of the commanding protocol class.

The subclass shall define the following:

Methods:

Name Description after_serve

This method needs to be overridden by the subclass if certain actions need to be executed after the control

before_serve

This method needs to be overridden by the subclass if certain actions need to be executed before the control

get_average_execution_times

Returns the average execution times of all functions that have been monitored by this process.

get_commanding_port

Returns the commanding port used by the Control Server.

get_communication_protocol

Returns the communication protocol used by the Control Server.

get_ip_address

Returns the IP address of the current host.

get_monitoring_port

Returns the monitoring port used by the Control Server.

get_process_status

Returns the process status of the Control Server.

get_service_port

Returns the service port used by the Control Server.

get_storage_mnemonic

Returns the storage mnemonics used by the Control Server.

handle_scheduled_tasks

Executes or reschedules tasks in the serve() event loop.

is_storage_manager_active

Checks if the Storage Manager is active.

notify_listeners

Notifies registered listeners about an event.

quit

Interrupts the Control Server.

register_as_listener

Registers a listener with the specified proxy.

register_to_storage_manager

Registers this Control Server to the Storage Manager.

schedule_task

Schedules a task to run in the control server event loop.

serve

Activation of the Control Server.

set_hk_delay

Sets the delay time for housekeeping.

set_logging_level

Sets the logging level to the given level.

set_mon_delay

Sets the delay time for monitoring.

set_scheduled_task_delay

Sets the delay time between successive executions of scheduled tasks.

store_housekeeping_information

Sends housekeeping information to the Storage Manager.

unregister_as_listener

Removes a registered listener from the specified proxy.

unregister_from_storage_manager

Unregisters the Control Server from the Storage Manager.

"},{"location":"api/control/#egse.control.ControlServer.after_serve","title":"after_serve","text":"
after_serve()\n

This method needs to be overridden by the subclass if certain actions need to be executed after the control server has been deactivated.

"},{"location":"api/control/#egse.control.ControlServer.before_serve","title":"before_serve","text":"
before_serve()\n

This method needs to be overridden by the subclass if certain actions need to be executed before the control server is activated.

"},{"location":"api/control/#egse.control.ControlServer.get_average_execution_times","title":"get_average_execution_times","text":"
get_average_execution_times()\n

Returns the average execution times of all functions that have been monitored by this process.

Returns:

Type Description dict

Dictionary with the average execution times of all functions that have been monitored by this process. The dictionary keys are the function names, and the values are the average execution times in ms.

"},{"location":"api/control/#egse.control.ControlServer.get_commanding_port","title":"get_commanding_port abstractmethod","text":"
get_commanding_port()\n

Returns the commanding port used by the Control Server.

Returns:

Type Description int

Commanding port used by the Control Server, as specified in the settings.

"},{"location":"api/control/#egse.control.ControlServer.get_communication_protocol","title":"get_communication_protocol abstractmethod","text":"
get_communication_protocol()\n

Returns the communication protocol used by the Control Server.

Returns:

Type Description str

Communication protocol used by the Control Server, as specified in the settings.

"},{"location":"api/control/#egse.control.ControlServer.get_ip_address","title":"get_ip_address","text":"
get_ip_address()\n

Returns the IP address of the current host.

"},{"location":"api/control/#egse.control.ControlServer.get_monitoring_port","title":"get_monitoring_port abstractmethod","text":"
get_monitoring_port()\n

Returns the monitoring port used by the Control Server.

Returns:

Type Description int

Monitoring port used by the Control Server, as specified in the settings.

"},{"location":"api/control/#egse.control.ControlServer.get_process_status","title":"get_process_status","text":"
get_process_status()\n

Returns the process status of the Control Server.

Returns:

Type Description dict

Dictionary with the process status of the Control Server.

"},{"location":"api/control/#egse.control.ControlServer.get_service_port","title":"get_service_port abstractmethod","text":"
get_service_port()\n

Returns the service port used by the Control Server.

Returns:

Type Description int

Service port used by the Control Server, as specified in the settings.

"},{"location":"api/control/#egse.control.ControlServer.get_storage_mnemonic","title":"get_storage_mnemonic","text":"
get_storage_mnemonic()\n

Returns the storage mnemonics used by the Control Server.

This is a string that will appear in the filename with the housekeeping information of the device, as a way of identifying the device. If this is not implemented in the subclass, then the class name will be used.

Returns:

Type Description str

Storage mnemonics used by the Control Server, as specified in the settings.

"},{"location":"api/control/#egse.control.ControlServer.handle_scheduled_tasks","title":"handle_scheduled_tasks","text":"
handle_scheduled_tasks()\n

Executes or reschedules tasks in the serve() event loop.

"},{"location":"api/control/#egse.control.ControlServer.is_storage_manager_active","title":"is_storage_manager_active","text":"
is_storage_manager_active()\n

Checks if the Storage Manager is active.

This method has to be implemented by the subclass if you need to store information.

Note: You might want to set a specific timeout when checking for the Storage Manager.

Note: If this method returns True, the following methods shall also be implemented by the subclass:

Returns:

Type Description bool

True if the Storage Manager is active; False otherwise.

"},{"location":"api/control/#egse.control.ControlServer.notify_listeners","title":"notify_listeners","text":"
notify_listeners(event_id=0, context=None)\n

Notifies registered listeners about an event.

This function creates an Event object with the provided event_id and context and notifies all registered listeners with the created event.

Parameters:

Name Type Description Default event_id int

The identifier for the event. Defaults to 0.

0 context dict

Additional context information associated with the event. Defaults to None.

None Note

The notification is performed by the notify_listeners method of the listeners object associated with this instance. The notification is executed in a daemon thread to avoid blocking the commanding chain.

"},{"location":"api/control/#egse.control.ControlServer.quit","title":"quit","text":"
quit()\n

Interrupts the Control Server.

"},{"location":"api/control/#egse.control.ControlServer.register_as_listener","title":"register_as_listener","text":"
register_as_listener(proxy, listener)\n

Registers a listener with the specified proxy.

This function attempts to add the provided listener to the specified proxy. It employs a retry mechanism to handle potential ConnectionError exceptions, making up to 5 attempts to add the listener.

Parameters:

Name Type Description Default proxy Type

A callable object representing the proxy to which the listener will be added.

required listener dict

The listener to be registered. Should be a dictionary containing listener details.

required

Raises:

Type Description ConnectionError

If the connection to the proxy encounters issues even after multiple retry attempts.

Note

The function runs in a separate daemon thread to avoid blocking the main thread.

"},{"location":"api/control/#egse.control.ControlServer.register_to_storage_manager","title":"register_to_storage_manager","text":"
register_to_storage_manager()\n

Registers this Control Server to the Storage Manager.

By doing so, the housekeeping information of the device will be sent to the Storage Manager, which will store the information in a dedicated CSV file.

This method has to be overwritten by the subclasses if they have housekeeping information that must be stored.

Subclasses need to overwrite this method if they have housekeeping information to be stored.

The following information is required for the registration:

The egse.storage module provides a convenience method that can be called from the method in the subclass:

>>> from egse.storage import register_to_storage_manager  # noqa\n
Note

the egse.storage module might not be available, it is provided by the cgse-core package.

"},{"location":"api/control/#egse.control.ControlServer.schedule_task","title":"schedule_task","text":"
schedule_task(callback, after=0.0, when=None)\n

Schedules a task to run in the control server event loop.

The callback function will be executed as soon as possible in the serve() event loop.

Some simple scheduling options are available:

The after and the when arguments can be combined.

Note "},{"location":"api/control/#egse.control.ControlServer.serve","title":"serve","text":"
serve()\n

Activation of the Control Server.

This comprises the following steps:

"},{"location":"api/control/#egse.control.ControlServer.set_hk_delay","title":"set_hk_delay","text":"
set_hk_delay(seconds)\n

Sets the delay time for housekeeping.

The delay time is the time between two successive executions of the get_housekeeping() function of the device protocol.

It might happen that the delay time that is set is longer than what you requested. That is the case when the execution of the get_housekeeping() function takes longer than the requested delay time. That should prevent the server from blocking when a too short delay time is requested.

Parameters:

Name Type Description Default seconds float

Number of seconds between the housekeeping calls

required

Returns:

Type Description float

Delay that was set [ms].

"},{"location":"api/control/#egse.control.ControlServer.set_logging_level","title":"set_logging_level","text":"
set_logging_level(level)\n

Sets the logging level to the given level.

Allowed logging levels are:

Parameters:

Name Type Description Default level int | str

Logging level to use, specified as either a string or an integer

required"},{"location":"api/control/#egse.control.ControlServer.set_mon_delay","title":"set_mon_delay","text":"
set_mon_delay(seconds)\n

Sets the delay time for monitoring.

The delay time is the time between two successive executions of the get_status() function of the device protocol.

It might happen that the delay time that is set is longer than what you requested. That is the case when the execution of the get_status() function takes longer than the requested delay time. That should prevent the server from blocking when a too short delay time is requested.

Parameters:

Name Type Description Default seconds float

Number of seconds between the monitoring calls

required

Returns:

Type Description float

Delay that was set [ms].

"},{"location":"api/control/#egse.control.ControlServer.set_scheduled_task_delay","title":"set_scheduled_task_delay","text":"
set_scheduled_task_delay(seconds)\n

Sets the delay time between successive executions of scheduled tasks.

Parameters:

Name Type Description Default seconds

the time interval between two successive executions [seconds]

required"},{"location":"api/control/#egse.control.ControlServer.store_housekeeping_information","title":"store_housekeeping_information","text":"
store_housekeeping_information(data)\n

Sends housekeeping information to the Storage Manager.

This method has to be overwritten by the subclasses if they want the device housekeeping information to be saved.

Parameters:

Name Type Description Default data dict

a dictionary containing parameter name and value of all device housekeeping. There is also a timestamp that represents the date/time when the HK was received from the device.

required"},{"location":"api/control/#egse.control.ControlServer.unregister_as_listener","title":"unregister_as_listener","text":"
unregister_as_listener(proxy, listener)\n

Removes a registered listener from the specified proxy.

This function attempts to remove the provided listener from the specified proxy. It employs a retry mechanism to handle potential ConnectionError exceptions, making up to 5 attempts to add the listener.

Parameters:

Name Type Description Default proxy Type

A callable object representing the proxy from which the listener will be removed.

required listener dict

The listener to be removed. Should be a dictionary containing listener details.

required

Raises:

Type Description ConnectionError

If the connection to the proxy encounters issues even after multiple retry attempts.

Note

The function runs in a separate thread but will block until the de-registration is finished. The reason being that this method is usually called in a after_serve block so it needs to finish before the ZeroMQ context is destroyed.

"},{"location":"api/control/#egse.control.ControlServer.unregister_from_storage_manager","title":"unregister_from_storage_manager","text":"
unregister_from_storage_manager()\n

Unregisters the Control Server from the Storage Manager.

This method has to be overwritten by the subclasses.

The following information is required for the registration:

The egse.storage module provides a convenience method that can be called from the method in the subclass:

>>> from egse.storage import unregister_from_storage_manager  # noqa\n
Note

the egse.storage module might not be available, it is provided by the cgse-core package.

"},{"location":"api/control/#egse.control.is_control_server_active","title":"is_control_server_active","text":"
is_control_server_active(endpoint=None, timeout=0.5)\n

Checks if the Control Server is running.

This function sends a Ping message to the Control Server and expects a Pong answer back within the timeout period.

Parameters:

Name Type Description Default endpoint str

Endpoint to connect to, i.e. ://: None timeout float

Timeout when waiting for a reply [s, default=0.5]

0.5"},{"location":"api/counter/","title":"egse.counter","text":"

This module manages files that have a counter in their filename.

Functions:

Name Description counter_exists

Returns True if the given file exists, False otherwise.

counter_filename

Creates an absolute filename to be used as a counter file. A counter file usually has a 'count' extension

get_next_counter

Read the counter from a dedicated file, add one and save the counter back to the file.

new_counter

Create a counter based on the files that already exist for the given pattern.

"},{"location":"api/counter/#egse.counter.counter_exists","title":"counter_exists","text":"
counter_exists(filename)\n

Returns True if the given file exists, False otherwise.

Parameters:

Name Type Description Default filename Path

path of the counter file

required

Returns:

Type Description bool

True if the given filename exists, False otherwise.

Note

No checking is done if the file is indeed a counter file, i.e. if it contains the correct content. So, this function basically only checks if the given Path exists and if it is a regular file.

"},{"location":"api/counter/#egse.counter.counter_filename","title":"counter_filename","text":"
counter_filename(location, filename)\n

Creates an absolute filename to be used as a counter file. A counter file usually has a 'count' extension but that is not enforced by this module. The location can be a relative path, even '.' or '..' are accepted.

Parameters:

Name Type Description Default location Path

the location of the counter file.

required filename Path | str

the name of the counter file, use the '.count' extension.

required

Returns:

Type Description Path

An absolute filename.

Note

If the file doesn't exist, it is NOT created.

"},{"location":"api/counter/#egse.counter.determine_counter_from_dir_list","title":"determine_counter_from_dir_list","text":"
determine_counter_from_dir_list(\n    location, pattern, index=-1\n)\n

Determine counter for a new file at the given location and with the given pattern. The next counter is determined from the sorted list of files that match the given pattern.

Parameters:

Name Type Description Default location

Location where the file should be stored.

required pattern

Pattern for the filename.

required index int

the location of the counter in the filename after it is split on '_' [default=-1]

-1

Returns:

Type Description int

The value of the next counter, 1 if no previous files were found or if an error occurred.

"},{"location":"api/counter/#egse.counter.get_next_counter","title":"get_next_counter","text":"
get_next_counter(filename)\n

Read the counter from a dedicated file, add one and save the counter back to the file.

Parameters:

Name Type Description Default file_path

full pathname of the file that contains the required counter

required

Returns:

Type Description int

The value of the next counter, 1 if no previous files were found or if an error occurred.

Note

This will create the counter file if it doesn't exist.

"},{"location":"api/counter/#egse.counter.new_counter","title":"new_counter","text":"
new_counter(filename, pattern)\n

Create a counter based on the files that already exist for the given pattern.

Parameters:

Name Type Description Default filename Path

the name of the counter file

required pattern str

a pattern to match the filenames

required

Returns:

Type Description int

The next counter value as an integer.

"},{"location":"api/decorators/","title":"egse.decorators","text":"

A collection of useful decorator functions.

Classes:

Name Description Nothing

Just to get a nice repr for Nothing. It is kind of a Null object...

Profiler

A simple profiler class that provides some useful functions to profile a function.

classproperty

Defines a read-only class property.

Functions:

Name Description average_time

This is a decorator that is intended mainly as a development aid. When you decorate your function with

borg

Use the Borg pattern to make a class with a shared state between its instances and subclasses.

debug

Logs the function signature and return value.

deprecate

Deprecate a function or method. This will print a warning with the function name and where

dynamic_interface

Adds a static variable __dynamic_interface to a method.

profile

Prints the function signature and return value to stdout.

profile_func

A time profiler decorator.

query_command

Adds a static variable __query_command to a method.

read_command

Adds a static variable __read_command to a method.

retry

Decorator that retries a function multiple times with a delay between attempts.

retry_with_exponential_backoff

Decorator for retrying a function with exponential backoff.

singleton

Use class as a singleton.

spy_on_attr_change

Tweak an object to show attributes changing. The changes are reported as WARNING log messages

static_vars

Define static variables in a function.

time_it

Print the runtime of the decorated function.

timer

Print the runtime of the decorated function.

to_be_implemented

Print a warning message that this function/method has to be implemented.

transaction_command

Adds a static variable __transaction_command to a method.

write_command

Adds a static variable __write_command to a method.

"},{"location":"api/decorators/#egse.decorators.Nothing","title":"Nothing","text":"

Just to get a nice repr for Nothing. It is kind of a Null object...

"},{"location":"api/decorators/#egse.decorators.Profiler","title":"Profiler","text":"

A simple profiler class that provides some useful functions to profile a function.

Examples:

>>> from egse.decorators import Profiler\n>>> @Profiler.count()\n... def square(x):\n...     return x**2\n
>>> x = [square(x) for x in range(1_000_000)]\n
>>> print(f\"Function 'square' called {square.get_count()} times.\")\n>>> print(square)\n
>>> @Profiler.duration()\n... def square(x):\n...     time.sleep(0.1)\n...     return x**2\n
>>> x = [square(x) for x in range(100)]\n
>>> print(f\"Function 'square' takes on average {square.get_average_duration():.6f} seconds.\")\n>>> print(square)\n
"},{"location":"api/decorators/#egse.decorators.classproperty","title":"classproperty","text":"
classproperty(func)\n

Defines a read-only class property.

Examples:

>>> class Message:\n...     def __init__(self, msg):\n...         self._msg = msg\n...\n...     @classproperty\n...     def name(cls):\n...         return cls.__name__\n\n>>> msg = Message(\"a simple doctest\")\n>>> assert \"Message\" == msg.name\n
"},{"location":"api/decorators/#egse.decorators.average_time","title":"average_time","text":"
average_time(\n    *, name=\"average_time\", level=INFO, precision=6\n)\n

This is a decorator that is intended mainly as a development aid. When you decorate your function with @average_time, the execution time of your function will be kept and accumulated. At anytime in your code, you can request the total execution time and the number of calls:

@average_time()\ndef my_function():\n    ...\ntotal_execution_time, call_count = my_function.report()\n

Requesting the report will automatically log the average runtime and the number of calls. If you need to reset the execution time and the number of calls during your testing, use:

my_function.reset()\n

Parameters:

Name Type Description Default name str

A name for the timer that will be used during reporting, default='average_time'

'average_time' level int

the required log level, default=logging.INFO

INFO precision int

the precision used to report the average time, default=6

6

Returns:

Type Description

The decorated function.

"},{"location":"api/decorators/#egse.decorators.borg","title":"borg","text":"
borg(cls)\n

Use the Borg pattern to make a class with a shared state between its instances and subclasses.

from

we don't need no singleton

"},{"location":"api/decorators/#egse.decorators.debug","title":"debug","text":"
debug(func)\n

Logs the function signature and return value.

"},{"location":"api/decorators/#egse.decorators.deprecate","title":"deprecate","text":"
deprecate(reason=None, alternative=None)\n

Deprecate a function or method. This will print a warning with the function name and where it is called from. If the optional parameters reason and alternative are given, that information will be printed with the warning.

Examples:

@deprecate(reason=\"it doesn't follow PEP8\", alternative=\"set_color()\")\ndef setColor(self, color):\n    self.set_color(color)\n

Parameters:

Name Type Description Default reason Optional[str]

provide a short explanation why this function is deprecated. Generates 'because {reason}'

None alternative Optional[str]

provides an alternative function/parameters to be used. Generates 'Use {alternative}

None

Returns:

Type Description Callable

The decorated function.

"},{"location":"api/decorators/#egse.decorators.dynamic_interface","title":"dynamic_interface","text":"
dynamic_interface(func)\n

Adds a static variable __dynamic_interface to a method.

The intended use of this function is as a decorator for functions in an interface class.

The static variable is currently used by the Proxy class to check if a method is meant to be overridden dynamically. The idea behind this is to loosen the contract of an abstract base class (ABC) into an interface. For an ABC, the abstract methods must be implemented at construction/initialization. This is not possible for the Proxy subclasses as they load their commands (i.e. methods) from the control server, and the method will be added to the Proxy interface after loading. Nevertheless, we like the interface already defined for auto-completion during development or interactive use.

When a Proxy subclass that implements an interface with methods decorated by the @dynamic_interface does overwrite one or more of the decorated methods statically, these methods will not be dynamically overwritten when loading the interface from the control server. A warning will be logged instead.

"},{"location":"api/decorators/#egse.decorators.profile","title":"profile","text":"
profile(func)\n

Prints the function signature and return value to stdout.

This function checks the Settings.profiling() value and only prints out profiling information if this returns True.

Profiling can be activated with Settings.set_profiling(True).

"},{"location":"api/decorators/#egse.decorators.profile_func","title":"profile_func","text":"
profile_func(\n    output_file=None,\n    sort_by=\"cumulative\",\n    lines_to_print=None,\n    strip_dirs=False,\n)\n

A time profiler decorator.

Parameters:

Name Type Description Default output_file

str or None. Default is None Path of the output file. If only name of the file is given, it's saved in the current directory. If it's None, the name of the decorated function is used.

None sort_by

str or SortKey enum or tuple/list of str/SortKey enum Sorting criteria for the Stats object. For a list of valid string and SortKey refer to: https://docs.python.org/3/library/profile.html#pstats.Stats.sort_stats

'cumulative' lines_to_print

int or None Number of lines to print. Default (None) is for all the lines. This is useful in reducing the size of the printout, especially that sorting by 'cumulative', the time consuming operations are printed toward the top of the file.

None strip_dirs

bool Whether to remove the leading path info from file names. This is also useful in reducing the size of the printout

False

Returns:

Type Description

Profile of the decorated function

Note

This code was taken from this gist: a profile decorator.

Inspired by and modified the profile decorator of Giampaolo Rodola: profile decorato.

"},{"location":"api/decorators/#egse.decorators.query_command","title":"query_command","text":"
query_command(func)\n

Adds a static variable __query_command to a method.

"},{"location":"api/decorators/#egse.decorators.read_command","title":"read_command","text":"
read_command(func)\n

Adds a static variable __read_command to a method.

"},{"location":"api/decorators/#egse.decorators.retry","title":"retry","text":"
retry(times=3, wait=10.0, exceptions=None)\n

Decorator that retries a function multiple times with a delay between attempts.

This decorator can be applied to a function to handle specified exceptions by retrying the function execution. It will make up to 'times' attempts with a waiting period of 'wait' seconds between each attempt. Any exception from the list provided in the exceptions argument will be ignored for the given times.

If after times attempts still an exception is raised, it will be passed through the calling function, otherwise the functions return value will be returned.

Parameters:

Name Type Description Default times int

The number of retry attempts. Defaults to 3.

3 wait float

The waiting period between retries in seconds. Defaults to 10.0.

10.0 exceptions List[Exception] or None

List of exception types to catch and retry. Defaults to None, which catches all exceptions.

None

Returns:

Name Type Description Callable

The decorated function.

Example

Apply the retry decorator to a function with specific retry settings:

@retry(times=5, wait=15.0, exceptions=[ConnectionError, TimeoutError])\ndef my_function():\n    # Function logic here\n
Note

The decorator catches specified exceptions and retries the function, logging information about each retry attempt.

"},{"location":"api/decorators/#egse.decorators.retry_with_exponential_backoff","title":"retry_with_exponential_backoff","text":"
retry_with_exponential_backoff(\n    max_attempts=5,\n    initial_wait=1.0,\n    backoff_factor=2,\n    exceptions=None,\n)\n

Decorator for retrying a function with exponential backoff.

This decorator can be applied to a function to handle specified exceptions by retrying the function execution. It will make up to 'max_attempts' attempts with a waiting period that grows exponentially between each attempt (dependent on the backoff_factor). Any exception from the list provided in the exceptions argument will be ignored for the given max_attempts.

If after all attempts still an exception is raised, it will be passed through the calling function, otherwise the functions return value will be returned.

Parameters:

Name Type Description Default max_attempts

The maximum number of attempts to make.

5 initial_wait

The initial waiting time in seconds before retrying after the first failure.

1.0 backoff_factor

The factor by which the wait time increases after each failure.

2

Returns:

Type Description

The response from the executed function.

"},{"location":"api/decorators/#egse.decorators.singleton","title":"singleton","text":"
singleton(cls)\n

Use class as a singleton.

from

Decorator library: Signleton

"},{"location":"api/decorators/#egse.decorators.spy_on_attr_change","title":"spy_on_attr_change","text":"
spy_on_attr_change(obj, obj_name=None)\n

Tweak an object to show attributes changing. The changes are reported as WARNING log messages in the egse.spy logger.

Note this is not a decorator, but a function that changes the class of an object.

Note that this function is a debugging aid and should not be used in production code!

Parameters:

Name Type Description Default obj object

any object that you want to monitor

required obj_name str

the variable name of the object that was given in the code, if None than the class name will be printed.

None Example
class X:\n   pass\n\nx = X()\nspy_on_attr_change(x, obj_name=\"x\")\nx.a = 5\n
From

Adding a dunder to an object

"},{"location":"api/decorators/#egse.decorators.static_vars","title":"static_vars","text":"
static_vars(**kwargs)\n

Define static variables in a function.

The static variable can be accessed with . inside the function body. Example

@static_vars(count=0)\ndef special_count():\n    return special_count.count += 2\n
"},{"location":"api/decorators/#egse.decorators.time_it","title":"time_it","text":"
time_it(count=1000, precision=4)\n

Print the runtime of the decorated function.

This is a simple replacement for the builtin timeit function. The purpose is to simplify calling a function with some parameters.

The intended way to call this is as a function:

value = function(args)\n\nvalue = time_it(10_000)(function)(args)\n

The time_it function can be called as a decorator in which case it will always call the function count times which is probably not what you want.

Parameters:

Name Type Description Default count int

the number of executions [default=1000].

1000 precision int

the number of significant digits [default=4]

4

Returns:

Name Type Description value

the return value of the last function execution.

See also

the Timer context manager located in egse.system.

Usage
@time_it(count=10000)\ndef function(args):\n    pass\n\ntime_it(10000)(function)(args)\n
"},{"location":"api/decorators/#egse.decorators.timer","title":"timer","text":"
timer(*, name='timer', level=INFO, precision=4)\n

Print the runtime of the decorated function.

Parameters:

Name Type Description Default name str

a name for the Timer, will be printed in the logging message

'timer' level int

the logging level for the time message [default=INFO]

INFO precision int

the number of decimals for the time [default=3 (ms)]

4"},{"location":"api/decorators/#egse.decorators.to_be_implemented","title":"to_be_implemented","text":"
to_be_implemented(func)\n

Print a warning message that this function/method has to be implemented.

"},{"location":"api/decorators/#egse.decorators.transaction_command","title":"transaction_command","text":"
transaction_command(func)\n

Adds a static variable __transaction_command to a method.

"},{"location":"api/decorators/#egse.decorators.write_command","title":"write_command","text":"
write_command(func)\n

Adds a static variable __write_command to a method.

"},{"location":"api/exceptions/","title":"egse.exceptions","text":"
Exception\n \u251c\u2500\u2500 CGSEException\n \u2502    \u251c\u2500\u2500 Warning\n \u2502    \u2514\u2500\u2500 Error\n \u2502        \u251c\u2500\u2500 InvalidOperationError\n \u2502        \u251c\u2500\u2500 DeviceNotFoundError\n \u2502        \u251c\u2500\u2500 InternalStateError\n \u2502        \u2514\u2500\u2500 DeviceError\n \u2502             \u251c\u2500\u2500 DeviceControllerError\n \u2502             \u251c\u2500\u2500 DeviceConnectionError\n \u2502             \u251c\u2500\u2500 DeviceTimeoutError\n \u2502             \u2514\u2500\u2500 DeviceInterfaceError\n \u251c\u2500\u2500 Failure\n \u251c\u2500\u2500 HexapodError\n \u251c\u2500\u2500 PMACError\n \u251c\u2500\u2500 OGSEError\n \u251c\u2500\u2500 ESLError\n \u251c\u2500\u2500 FilterWheelError\n \u251c\u2500\u2500 FilterWheel8smc4Error\n \u251c\u2500\u2500 ShutterKSC1010Error\n \u251c\u2500\u2500 WindowSizeError\n \u251c\u2500\u2500 SettingsError\n \u2514\u2500\u2500 StagesError\n

Classes:

Name Description Abort

Internal Exception to signal a process to abort.

CGSEException

The base exception for all errors and warnings in the Common-EGSE.

DeviceNotFoundError

Raised when a device could not be located, or loaded.

Error

The base class for all Common-EGSE Errors.

FileIsEmptyError

Raised when a file is empty and that is unexpected.

InternalError

Raised when an internal inconsistency occurred in a function, method or class.

InternalStateError

Raised when an object encounters an internal state inconsistency.

InvalidInputError

Exception raised when the input is invalid after editing.

InvalidOperationError

Raised when a certain operation is not valid in the given state,

Warning

The base class for all Common-EGSE Warnings.

"},{"location":"api/exceptions/#egse.exceptions.Abort","title":"Abort","text":"

Bases: RuntimeError

Internal Exception to signal a process to abort.

"},{"location":"api/exceptions/#egse.exceptions.CGSEException","title":"CGSEException","text":"

Bases: Exception

The base exception for all errors and warnings in the Common-EGSE.

"},{"location":"api/exceptions/#egse.exceptions.DeviceNotFoundError","title":"DeviceNotFoundError","text":"

Bases: Error

Raised when a device could not be located, or loaded.

"},{"location":"api/exceptions/#egse.exceptions.Error","title":"Error","text":"

Bases: CGSEException

The base class for all Common-EGSE Errors.

"},{"location":"api/exceptions/#egse.exceptions.FileIsEmptyError","title":"FileIsEmptyError","text":"

Bases: Error

Raised when a file is empty and that is unexpected.

"},{"location":"api/exceptions/#egse.exceptions.InternalError","title":"InternalError","text":"

Bases: Error

Raised when an internal inconsistency occurred in a function, method or class.

"},{"location":"api/exceptions/#egse.exceptions.InternalStateError","title":"InternalStateError","text":"

Bases: Error

Raised when an object encounters an internal state inconsistency.

"},{"location":"api/exceptions/#egse.exceptions.InvalidInputError","title":"InvalidInputError","text":"

Bases: Error

Exception raised when the input is invalid after editing.

"},{"location":"api/exceptions/#egse.exceptions.InvalidOperationError","title":"InvalidOperationError","text":"

Bases: Error

Raised when a certain operation is not valid in the given state, circumstances or environment.

"},{"location":"api/exceptions/#egse.exceptions.Warning","title":"Warning","text":"

Bases: CGSEException

The base class for all Common-EGSE Warnings.

"},{"location":"api/settings/","title":"egse.settings","text":"

The Settings class handles user and configuration settings that are provided in a YAML file.

The idea is that settings are grouped by components or any arbitrary grouping that makes sense for the application or for the user. Settings are also modular and provided by each package by means of entry-points. The Settings class can read from different YAML files.

By default, settings are loaded from a file called settings.yaml, but this can be changed in the entry-point definition.

The yaml configuration files are provided as entry points by the packages that specified an entry-point group 'cgse.settings' in the pyproject.toml. The Settings dictionary (attrdict) is constructed from the configuration YAML files from each of the packages. Settings can be overwritten by the next package configuration file. So, make sure the group names in each package configuration file are unique.

The YAML file is read and the configuration parameters for the given group are available as instance variables of the returned class.

The intended use is as follows:

from egse.settings import Settings\n\ndsi_settings = Settings.load(\"DSI\")\n\nif 0x000C <= dsi_settings.RMAP_BASE_ADDRESS <= 0x00FF:\n    ...  # do something here\nelse:\n    raise RMAPError(\"Attempt to access outside the RMAP memory map.\")\n

The above code reads the settings from the default YAML file for a group called DSI. The settings will then be available as variables of the returned class, in this case dsi_settings. The returned class is and behaves also like a dictionary, so you can check if a configuration parameter is defined like this:

if \"DSI_FEE_IP_ADDRESS\" not in dsi_settings:\n    # define the IP address of the DSI\n

The YAML section for the above code looks like this:

DSI:\n\n    # DSI Specific Settings\n\n    DSI_FEE_IP_ADDRESS  10.33.178.144   # IP address of the DSI EtherSpaceLink interface\n    LINK_SPEED:                   100   # SpW link speed used for both up- and downlink\n\n    # RMAP Specific Settings\n\n    RMAP_BASE_ADDRESS:     0x00000000   # The start of the RMAP memory map managed by the FEE\n    RMAP_MEMORY_SIZE:            4096   # The size of the RMAP memory map managed by the FEE\n

When you want to read settings from another YAML file, specify the filename= keyword. If that file is located at a specific location, also use the location= keyword.

my_settings = Settings.load(filename=\"user.yaml\", location=\"/Users/JohnDoe\")\n

The above code will read the YAML file from the given location and not from the entry-points.

Classes:

Name Description Settings

The Settings class provides a load() method that loads configuration settings for a group

SettingsError

A settings-specific error.

Functions:

Name Description load_global_settings

Loads the settings that are defined by the given entry_point. The entry-points are defined in the

load_local_settings

Loads the local settings file that is defined from the environment variable PROJECT_LOCAL_SETTINGS (where

load_settings_file

Loads the YAML configuration file that is located at path / filename.

read_configuration_file

Read the YAML input configuration file. The configuration file is only read

"},{"location":"api/settings/#egse.settings.Settings","title":"Settings","text":"

The Settings class provides a load() method that loads configuration settings for a group into a dynamically created class as instance variables.

Methods:

Name Description load

Load the settings for the given group. When no group is provided, the

to_string

Returns a simple string representation of the cached configuration of this Settings class.

"},{"location":"api/settings/#egse.settings.Settings.load","title":"load classmethod","text":"
load(\n    group_name=None,\n    filename=\"settings.yaml\",\n    location=None,\n    *,\n    add_local_settings=True,\n    force=False,\n)\n

Load the settings for the given group. When no group is provided, the complete configuration is returned.

The Settings are loaded from entry-points that are defined in each of the packages that provide a Settings file.

If a location is explicitly provided, the Settings will be loaded from that location, using the given filename or the default (which is settings.yaml).

Parameters:

Name Type Description Default group_name str

the name of one of the main groups from the YAML file

None filename str

the name of the YAML file to read [default=settings.yaml]

'settings.yaml' location (str, Path)

the path to the location of the YAML file

None force bool

force reloading the file

False add_local_settings bool

update the Settings with site specific local settings

True

Returns:

Type Description attrdict

a dynamically created class with the configuration parameters as instance variables.

Raises:

Type Description SettingsError

when the group is not defined in the YAML file.

"},{"location":"api/settings/#egse.settings.Settings.to_string","title":"to_string classmethod","text":"
to_string()\n

Returns a simple string representation of the cached configuration of this Settings class.

"},{"location":"api/settings/#egse.settings.SettingsError","title":"SettingsError","text":"

Bases: Exception

A settings-specific error.

"},{"location":"api/settings/#egse.settings.load_global_settings","title":"load_global_settings","text":"
load_global_settings(\n    entry_point=\"cgse.settings\", force=False\n)\n

Loads the settings that are defined by the given entry_point. The entry-points are defined in the pyproject.toml files of the packages that export their global settings.

Parameters:

Name Type Description Default entry_point str

the name of the entry-point group [default: 'cgse.settings']

'cgse.settings' force bool

force reloading the settings, i.e. ignore the cache

False

Returns:

Type Description attrdict

A dictionary (attrdict) containing a collection of all the settings exported by the packages through the given entry-point.

"},{"location":"api/settings/#egse.settings.load_local_settings","title":"load_local_settings","text":"
load_local_settings(force=False)\n

Loads the local settings file that is defined from the environment variable PROJECT_LOCAL_SETTINGS (where PROJECT is the name of your project, defined in the environment variable of the same name).

This function might return an empty dictionary when

in both cases a warning message is logged.

Raises:

Type Description SettingsError

when the local settings YAML file is not found. Check the PROJECT_LOCAL_SETTINGS environment variable.

Returns:

Type Description attrdict

A dictionary (attrdict) with all local settings.

"},{"location":"api/settings/#egse.settings.load_settings_file","title":"load_settings_file","text":"
load_settings_file(path, filename, force=False)\n

Loads the YAML configuration file that is located at path / filename.

Parameters:

Name Type Description Default path PATH

the folder where the YAML file is located

required filename str

the name of the YAML configuration file

required force bool

force reloading, i.e. don't use the cached information

False

Raises:

Type Description SettingsError

when the configuration file doesn't exist or cannot be found or when there was an error reading the configuration file.

Returns:

Type Description attrdict

A dictionary (attrdict) with all the settings from the given file.

Note

in case of an empty configuration file, and empty dictionary is returned and a warning message is issued.

"},{"location":"api/settings/#egse.settings.read_configuration_file","title":"read_configuration_file","text":"
read_configuration_file(filename, *, force=False)\n

Read the YAML input configuration file. The configuration file is only read once and memoized as load optimization.

Parameters:

Name Type Description Default filename Path

the fully qualified filename of the YAML file

required force bool

force reloading the file, even when it was memoized

False

Raises:

Type Description SettingsError

when there was an error reading the YAML file.

Returns:

Type Description dict

a dictionary containing all the configuration settings from the YAML file.

"},{"location":"api/setup/","title":"egse.setup","text":""},{"location":"api/setup/#egse.setup--setup","title":"Setup","text":"

This module defines the Setup, which contains the complete configuration information for a test.

The Setup class contains all configuration items that are specific for a test or observation and is normally (during nominal operation/testing) loaded automatically from the configuration manager. The Setup includes type and identification of hardware that is used, calibration files, software versions, reference frames and coordinate systems that link positions of alignment equipment, conversion functions for temperature sensors, etc.

The configuration information that is in the Setup can be navigated in two different ways. First, the Setup is a dictionary, so all information can be accessed by keys as in the following example.

>>> setup = Setup({\"gse\": {\"hexapod\": {\"ID\": 42, \"calibration\": [0,1,2,3,4,5]}}})\n>>> setup[\"gse\"][\"hexapod\"][\"ID\"]\n42\n

Second, each of the keys is also available as an attribute of the Setup and that make it possible to navigate the Setup with dot-notation:

>>> id = setup.gse.hexapod.ID\n

In the above example you can see how to navigate from the setup to a device like the PUNA Hexapod. The Hexapod device is connected to the control server and accepts commands as usual. If you want to know which keys you can use to navigate the Setup, use the keys() method.

>>> setup.gse.hexapod.keys()\ndict_keys(['ID', 'calibration'])\n>>> setup.gse.hexapod.calibration\n[0, 1, 2, 3, 4, 5]\n

To get a full printout of the Setup, you can use the pretty_str() method. Be careful, because this can print out a lot of information when a full Setup is loaded.

>>> print(setup)\nSetup\n\u2514\u2500\u2500 gse\n    \u2514\u2500\u2500 hexapod\n        \u251c\u2500\u2500 ID: 42\n        \u2514\u2500\u2500 calibration: [0, 1, 2, 3, 4, 5]\n
"},{"location":"api/setup/#egse.setup--special-values","title":"Special Values","text":"

Some of the information in the Setup is interpreted in a special way, i.e. some values are processed before returning. Examples are the device classes and calibration/data files. The following values are treated special if they start with:

"},{"location":"api/setup/#egse.setup--device-classes","title":"Device Classes","text":"

Most of the hardware components in the Setup will have a device key that defines the class for the device controller. The device keys have a value that starts with class// and it will return the device object. As an example, the following defines the Hexapod device:

>>> setup = Setup(\n...   {\n...     \"gse\": {\n...       \"hexapod\": {\"ID\": 42, \"device\": \"class//egse.hexapod.symetrie.puna.PunaSimulator\"}\n...     }\n...   }\n... )\n>>> setup.gse.hexapod.device.is_homing_done()\nFalse\n>>> setup.gse.hexapod.device.info()\n'Info about the PunaSimulator...'\n

In the above example you see that we can call the is_homing_done() and info() methodes directly on the device by navigating the Setup. It would however be better (more performant) to put the device object in a variable and work with that variable:

>>> hexapod = setup.gse.hexapod.device\n>>> hexapod.homing()\n>>> hexapod.is_homing_done()\nTrue\n>>> hexapod.get_user_positions()\n

If you need, for some reason, to have access to the actual raw value of the hexapod device key, use the get_raw_value() method:

>>> setup.gse.hexapod.get_raw_value(\"device\")\n<egse.hexapod.symetrie.puna.PunaSimulator object at ...\n
"},{"location":"api/setup/#egse.setup--data-files","title":"Data Files","text":"

Some information is too large to add to the Setup as such and should be loaded from a data file. Examples are calibration files, flat-fields, temperature conversion curves, etc.

The Setup will automatically load the file when you access a key that contains a value that starts with csv// or yaml//.

>>> setup = Setup({\n...     \"instrument\": {\"coeff\": \"csv//cal_coeff_1234.csv\"}\n... })\n>>> setup.instrument.coeff[0, 4]\n5.0\n

Note: the resource location is always relative to the path defined by the PROJECT_CONF_DATA_LOCATION environment variable.

The Setup inherits from a NavigableDict (aka navdict) which is also defined in this module.

Classes:

Name Description Setup

The Setup class represents a version of the configuration of the test facility, the

SetupError

A setup-specific error.

Functions:

Name Description get_setup

Retrieve the currently active Setup from the configuration manager.

list_setups

This is a function to be used for interactive use, it will print to the terminal (stdout) a

load_last_setup_id

Returns the ID of the last Setup that was used by the configuration manager.

load_setup

This function loads the Setup corresponding with the given setup_id.

save_last_setup_id

Makes the given Setup ID persistent, so it can be restored upon the next startup.

submit_setup

Submit the given Setup to the Configuration Manager.

Attributes:

Name Type Description navdict

Shortcut for NavigableDict and more Pythonic.

"},{"location":"api/setup/#egse.setup.navdict","title":"navdict module-attribute","text":"
navdict = NavigableDict\n

Shortcut for NavigableDict and more Pythonic.

"},{"location":"api/setup/#egse.setup.NavigableDict","title":"NavigableDict","text":"
NavigableDict(head=None, label=None)\n

Bases: dict

A NavigableDict is a dictionary where all keys in the original dictionary are also accessible as attributes to the class instance. So, if the original dictionary (setup) has a key \"site_id\" which is accessible as setup['site_id'], it will also be accessible as setup.site_id.

Examples:

>>> setup = NavigableDict({'site_id': 'KU Leuven', 'version': \"0.1.0\"})\n>>> assert setup['site_id'] == setup.site_id\n>>> assert setup['version'] == setup.version\n
Note

We always want all keys to be accessible as attributes, or none. That means all keys of the original dictionary shall be of type str.

label (str): a label or name that is used when printing the navdict\n

Methods:

Name Description add

Set a value for the given key.

get_private_attribute

Returns the value of the given private attribute.

get_raw_value

Returns the raw value of the given key.

has_private_attribute

Check if the given key is defined as a private attribute.

pretty_str

Returns a pretty string representation of the dictionary.

set_private_attribute

Sets a private attribute for this object.

"},{"location":"api/setup/#egse.setup.NavigableDict.add","title":"add","text":"
add(key, value)\n

Set a value for the given key.

If the value is a dictionary, it will be converted into a NavigableDict and the keys will become available as attributes provided that all the keys are strings.

Parameters:

Name Type Description Default key str

the name of the key / attribute to access the value

required value Any

the value to assign to the key

required"},{"location":"api/setup/#egse.setup.NavigableDict.get_private_attribute","title":"get_private_attribute","text":"
get_private_attribute(key)\n

Returns the value of the given private attribute.

Parameters:

Name Type Description Default key str

the name of the private attribute (must start with an underscore character).

required

Returns:

Type Description Any

the value of the private attribute given in key.

Note

Because of the implementation, this private attribute can also be accessed as a 'normal' attribute of the object. This use is however discouraged as it will make your code less understandable. Use the methods to access these 'private' attributes.

"},{"location":"api/setup/#egse.setup.NavigableDict.get_raw_value","title":"get_raw_value","text":"
get_raw_value(key)\n

Returns the raw value of the given key.

Some keys have special values that are interpreted by the AtributeDict class. An example is a value that starts with 'class//'. When you access these values, they are first converted from their raw value into their expected value, e.g. the instantiated object in the above example. This method allows you to access the raw value before conversion.

"},{"location":"api/setup/#egse.setup.NavigableDict.has_private_attribute","title":"has_private_attribute","text":"
has_private_attribute(key)\n

Check if the given key is defined as a private attribute.

Parameters:

Name Type Description Default key str

the name of a private attribute (must start with an underscore)

required"},{"location":"api/setup/#egse.setup.NavigableDict.pretty_str","title":"pretty_str","text":"
pretty_str(indent=0)\n

Returns a pretty string representation of the dictionary.

Parameters:

Name Type Description Default indent int

number of indentations (of four spaces)

0 Note

The indent argument is intended for the recursive call of this function.

"},{"location":"api/setup/#egse.setup.NavigableDict.set_private_attribute","title":"set_private_attribute","text":"
set_private_attribute(key, value)\n

Sets a private attribute for this object.

The name in key will be accessible as an attribute for this object, but the key will not be added to the dictionary and not be returned by methods like keys().

The idea behind this private attribute is to have the possibility to add status information or identifiers to this classes object that can be used by save() or load() methods.

Parameters:

Name Type Description Default key str

the name of the private attribute (must start with an underscore character).

required value

the value for this private attribute

required

Examples:

>>> setup = NavigableDict({'a': 1, 'b': 2, 'c': 3})\n>>> setup.set_private_attribute(\"_loaded_from_dict\", True)\n>>> assert \"c\" in setup\n>>> assert \"_loaded_from_dict\" not in setup\n>>> assert setup.get_private_attribute(\"_loaded_from_dict\") == True\n
"},{"location":"api/setup/#egse.setup.Setup","title":"Setup","text":"
Setup(nav_dict=None, label=None)\n

Bases: NavigableDict

The Setup class represents a version of the configuration of the test facility, the test setup and the Camera Under Test (CUT).

Methods:

Name Description find_devices

Returns a dictionary with the devices that are included in the setup. The keys

from_dict

Create a Setup from a given dictionary.

from_yaml_file

Loads a Setup from the given YAML file.

from_yaml_string

Loads a Setup from the given YAML string.

get_filename

Returns the filename for this Setup or None when no filename could be determined.

get_id

Returns the Setup ID (as a string) or None when no setup id could be identified.

to_yaml_file

Saves a NavigableDict to a YAML file.

walk

Walk through the given dictionary, in a recursive way, appending the leaf with

"},{"location":"api/setup/#egse.setup.Setup.find_devices","title":"find_devices staticmethod","text":"
find_devices(node, devices=None)\n

Returns a dictionary with the devices that are included in the setup. The keys in the dictionary are taken from the \"device_name\" entries in the setup file. The corresponding values in the dictionary are taken from the \"device\" entries in the setup file.

Parameters:

Name Type Description Default node NavigableDict

Dictionary in which to look for the devices (and their names).

required devices dict

Dictionary in which to include the devices in the setup.

None

Returns:

Type Description dict

Dictionary with the devices that are included in the setup.

"},{"location":"api/setup/#egse.setup.Setup.from_dict","title":"from_dict staticmethod","text":"
from_dict(my_dict)\n

Create a Setup from a given dictionary.

Remember that all keys in the given dictionary shall be of type 'str' in order to be accessible as attributes.

Examples:

>>> setup = Setup.from_dict({\"ID\": \"my-setup-001\", \"version\": \"0.1.0\"})\n>>> assert setup[\"ID\"] == setup.ID == \"my-setup-001\"\n
"},{"location":"api/setup/#egse.setup.Setup.from_yaml_file","title":"from_yaml_file cached staticmethod","text":"
from_yaml_file(filename=None, add_local_settings=True)\n

Loads a Setup from the given YAML file.

Parameters:

Name Type Description Default filename str

the path of the YAML file to be loaded

None add_local_settings bool

if local settings shall be loaded and override the settings from the YAML file.

True

Returns:

Type Description

a Setup that was loaded from the given location.

"},{"location":"api/setup/#egse.setup.Setup.from_yaml_string","title":"from_yaml_string staticmethod","text":"
from_yaml_string(yaml_content=None)\n

Loads a Setup from the given YAML string.

This method is mainly used for easy creation of Setups from strings during unit tests.

Parameters:

Name Type Description Default yaml_content str

a string containing YAML

None

Returns:

Type Description

a Setup that was loaded from the content of the given string.

"},{"location":"api/setup/#egse.setup.Setup.get_filename","title":"get_filename","text":"
get_filename()\n

Returns the filename for this Setup or None when no filename could be determined.

"},{"location":"api/setup/#egse.setup.Setup.get_id","title":"get_id","text":"
get_id()\n

Returns the Setup ID (as a string) or None when no setup id could be identified.

"},{"location":"api/setup/#egse.setup.Setup.to_yaml_file","title":"to_yaml_file","text":"
to_yaml_file(filename=None)\n

Saves a NavigableDict to a YAML file.

When no filename is provided, this method will look for a 'private' attribute _filename and use that to save the data.

Parameters:

Name Type Description Default filename str | Path

the path of the YAML file where to save the data

None Note

This method will overwrite the original or given YAML file and therefore you might lose proper formatting and/or comments.

"},{"location":"api/setup/#egse.setup.Setup.walk","title":"walk staticmethod","text":"
walk(node, key_of_interest, leaf_list)\n

Walk through the given dictionary, in a recursive way, appending the leaf with the given keyword to the given list.

Parameters:

Name Type Description Default node dict

Dictionary in which to look for leaves with the given keyword.

required key_of_interest

Key to look for in the leaves of the given dictionary.

required leaf_list

List to which to add the leaves with the given keyword.

required

Returns:

Type Description list

Given list with the leaves (with the given keyword) in the given dictionary appended to it.

"},{"location":"api/setup/#egse.setup.SetupError","title":"SetupError","text":"

Bases: Exception

A setup-specific error.

"},{"location":"api/setup/#egse.setup.disentangle_filename","title":"disentangle_filename","text":"
disentangle_filename(filename)\n

Returns the site_id and setup_id (as a tuple) that is extracted from the Setups filename.

Parameters:

Name Type Description Default filename str

the filename or fully qualified file path as a string.

required

Returns:

Type Description tuple

A tuple (site_id, setup_id).

"},{"location":"api/setup/#egse.setup.get_last_setup_id_file_path","title":"get_last_setup_id_file_path","text":"
get_last_setup_id_file_path(site_id=None)\n

Return the fully expanded file path of the file containing the last loaded Setup in the configuration manager. The default location for this file is the data storage location.

Parameters:

Name Type Description Default site_id str

The SITE identifier (overrides the SITE_ID environment variable)

None"},{"location":"api/setup/#egse.setup.get_path_of_setup_file","title":"get_path_of_setup_file","text":"
get_path_of_setup_file(setup_id, site_id)\n

Returns the Path to the last Setup file for the given site_id. The last Setup file is the file with the largest setup_id number.

This function needs the environment variable _CONF_REPO_LOCATION to be defined as the location of the repository with configuration data on your disk. If the repo is not defined, the configuration data location will be used instead.

Parameters:

Name Type Description Default setup_id int

the identifier for the requested Setup

required site_id str

the test house name, one of CSL, SRON, IAS, INTA

required

Returns:

Type Description Path

The full path to the requested Setup file.

Raises:

Type Description LookupError

when the environment variable is not set.

NotADirectoryError

when either the repository folder or the Setups folder doesn't exist.

FileNotFoundError

when no Setup file can be found for the given arguments.

"},{"location":"api/setup/#egse.setup.get_setup","title":"get_setup","text":"
get_setup(setup_id=None)\n

Retrieve the currently active Setup from the configuration manager.

When a setup_id is provided, that setup will be returned, but not loaded in the configuration manager. This function does NOT change the configuration manager.

This function is for interactive use and consults the configuration manager server. Don't use this within the test script, but use the GlobalState.setup property instead.

"},{"location":"api/setup/#egse.setup.list_setups","title":"list_setups","text":"
list_setups(**attr)\n

This is a function to be used for interactive use, it will print to the terminal (stdout) a list of Setups known at the Configuration Manager. This list is sorted with the most recent ( highest) value last.

The list can be restricted with key:value pairs (keyword arguments). This search mechanism allows us to find all Setups that adhere to the key:value pairs, e.g. to find all Setups for CSL at position 2, use:

>>> list_setups(site_id=\"CSL\", position=2)\n

To have a nested keyword search (i.e. search by gse.hexapod.ID) then pass in gse__hexapod__ID as the keyword argument. Replace the '.' notation with double underscores '__'.

>>> list_setups(gse__hexapod__ID=4)\n
"},{"location":"api/setup/#egse.setup.load_last_setup_id","title":"load_last_setup_id","text":"
load_last_setup_id(site_id=None)\n

Returns the ID of the last Setup that was used by the configuration manager. The file shall only contain the Setup ID which must be an integer on the first line of the file. If no such ID can be found, the Setup ID = 0 will be returned.

Parameters:

Name Type Description Default site_id str

The SITE identifier

None"},{"location":"api/setup/#egse.setup.load_setup","title":"load_setup","text":"
load_setup(setup_id=None, site_id=None, from_disk=False)\n

This function loads the Setup corresponding with the given setup_id.

Loading a Setup means:

When no setup_id is provided, the current Setup is loaded from the configuration manager.

Parameters:

Name Type Description Default setup_id int

the identifier for the Setup

None site_id str

the name of the test house

None from_disk bool

True if the Setup needs to be loaded from disk

False

Returns:

Type Description Setup

The requested Setup or None when the Setup could not be loaded from the configuration manager.

"},{"location":"api/setup/#egse.setup.save_last_setup_id","title":"save_last_setup_id","text":"
save_last_setup_id(setup_id, site_id=None)\n

Makes the given Setup ID persistent, so it can be restored upon the next startup.

Parameters:

Name Type Description Default setup_id int | str

The Setup identifier to be saved

required site_id str

The SITE identifier

None"},{"location":"api/setup/#egse.setup.submit_setup","title":"submit_setup","text":"
submit_setup(setup, description)\n

Submit the given Setup to the Configuration Manager.

When you submit a Setup, the Configuration Manager will save this Setup with the next (new) setup id and make this Setup the current Setup in the Configuration manager unless you have explicitly set replace=False in which case the current Setup will not be replaced with the new Setup.

Parameters:

Name Type Description Default setup Setup

a (new) Setup to submit to the configuration manager

required description str

one-liner to help identifying the Setup afterwards

required

Returns:

Type Description str | None

The Setup ID of the newly created Setup or None.

"},{"location":"dev_guide/","title":"Developer Guide","text":"

Welcome to the CGSE developer guide! An in-depth reference on how to contribute to the CGSE.

First thing to know is that this repository is actually a monorepo, meaning it contains a bunch of related but self-standing packages with a minimum of interdependencies. A monorepo can grow quite big and can contain a lot of packages that even different groups are working on. What they have in common is that they use the same guidelines and have the same or a very similar development workflow.

Don't confuse a monorepo with a monolith or a monolithic architecture. While a monorepo holds multiple related but more-or-less independent projects, a monolith is a traditional software application or architecture which is an often huge, self-contained and independent unit of code that is highly coupled and difficult to maintain.

Don't confuse a monorepo with microservices either. A microservice architecture contains units that run independently and are developed, scaled and deployed without affecting the other units or services. You can set up a monorepo containing all of your microservices with ease, one does not need the other, but they can perfectly go together.

"},{"location":"dev_guide/coding_style/","title":"Style Guide","text":"

This part of the developer guide contains instructions for coding styles that are adopted for this project.

The style guide that we use for this project is PEP8. This is the standard for Python code and all IDEs, parsers and code formatters understand and work with this standard. PEP8 leaves room for project specific styles. A good style guide that we can follow is the Google Style Guide.

The following sections will give the most used conventions with a few examples of good and bad.

"},{"location":"dev_guide/coding_style/#tldr","title":"TL;DR","text":"Type Style Example Classes CapWords ProcessManager, ImageViewer, CommandList, Observation, MetaData Methods & Functions lowercase with underscores get_value, set_mask, create_image Variables lowercase with underscores key, last_value, model, index, user_info Constants UPPERCASE with underscores MAX_LINES, BLACK, COMMANDING_PORT Modules & packages lowercase no underscores dataset, commanding, multiprocessing"},{"location":"dev_guide/coding_style/#general","title":"General","text":"

Note

You will sometimes see that we use one or two words between < > angle brakcets. That means you will have to replace that text AND the brackets with your own text. As an example, if you see --prompt <venv name>, replace this with something like --prompt cgse-venv.

"},{"location":"dev_guide/coding_style/#classes","title":"Classes","text":"

Always use CamelCase (Python uses CapWords) for class names. When using acronyms, keep them all UPPER case.

Good names are: Observation, CalibrationFile, MetaData, Message, ReferenceFrame, URLParser.

"},{"location":"dev_guide/coding_style/#methods-and-functions","title":"Methods and Functions","text":"

A function or a method does something (and should only do one thing, SRP=Single Responsibility Principle), it is an action, so the name should reflect that action.

Always use lowercase words separated with underscores.

Good names are: get_time_in_ms(), get_commanding_port(), is_connected(), parse_time(), setup_mask().

When working with legacy code or code from another project, names may be in camelCase (with the first letter a lower case letter). So we can in this case use also getCommandPort() or isConnected() as method and function names.

"},{"location":"dev_guide/coding_style/#variables","title":"Variables","text":"

Use the same naming convention as functions and methods, i.e. lowercase with underscores.

Good names are: key, value, user_info, model, last_value

Bad names: NSegments, outNoise

Take care not to use builtins: list, type, filter, lambda, map, dict, ...

Private variables (for classes) start with an underscore: _name or _total_n_args.

In the same spirit as method and function names, the variables can also be in camelCase for specific cases.

"},{"location":"dev_guide/coding_style/#constants","title":"CONSTANTS","text":"

Use ALL_UPPER_CASE with underscores for constants. Use constants always within a name space, not globally.

Good names: MAX_LINES, BLACK, YELLOW, ESL_LINK_MODE_DISABLED

"},{"location":"dev_guide/coding_style/#modules-and-packages","title":"Modules and Packages","text":"

Use simple words for modules, preferably just one word like datasets or commanding or storage or extensions. If two words are unavoidable, just concatenate them, like multiprocessing or sampledata or testdata. If needed for readability, use an underscore to separate the words, e.g. image_analysis.

"},{"location":"dev_guide/coding_style/#import-statements","title":"Import Statements","text":"

Be careful that you do not name any modules the same as a module in the Python standard library. This can result in strange effects and may result in an AttributeError. Suppose you have named a module math in the egse directory and it is imported and used further in the code as follows:

from egse import math\n\n# in some expression further down the code you might use\n\nmath.exp(a)\n

This will result in the following runtime error:

File \"some_module.py\", line 8, in <module>\n  print(math.exp(a))\nAttributeError: module 'egse.math' has no attribute 'exp'\n

Of course this is an obvious example, but it might be more obscure like e.g. in this GitHub issue: 'module' object has no attribute 'Cmd'.

"},{"location":"dev_guide/docs/","title":"Building the documentation","text":""},{"location":"dev_guide/docs/#set-up-your-environment","title":"Set up your environment","text":"

The pyproject.toml file of the cgse root contains additional dependencies for running the mkdocs commands. When working on the documentation, make sure you have installed the 'docs' dependency group. Currently, only mkdocs and mkdocs-material are needed. You can use the following command to add the documentation dependencies to your development environment.

$ cd ~/github/cgse\n$ uv sync --all-packages --all-groups\n

Now you can start the live-reload server of mkdocs. This will recreate the documentation whenever you make a change in the files below the docs folder. After starting this command, navigate to the http://127.0.0.1:8000/cgse/ site in your favorite browser.

$ uv run mkdocs serve\n

Now you can update files, create new folders in docs/*, create new Markdown files and all changes will be reloaded live in the browser.

When you are ready with updating, you will need to build the site and publish it on GitHub pages:

$ uv run mkdocs build\n$ uv run mkdocs gh-deploy -r upstream -m \"documentation update on ..\"\n
"},{"location":"dev_guide/docs/#commands","title":"Commands","text":""},{"location":"dev_guide/docs/#project-layout","title":"Project layout","text":"

The documentation pages follow more or less the structure of the code in terms of libs and projects. Below I have laid out this structure leaving out less important files and folders.

mkdocs.yml         # the mkdocs configuration file\ndocs\n\u251c\u2500\u2500 index.md       # the documentation homepage\n\u251c\u2500\u2500 initialize.md\n\u251c\u2500\u2500 getting_started.md\n\u251c\u2500\u2500 package_list.md\n\u251c\u2500\u2500 dev_guide/\n\u251c\u2500\u2500 user_guide/\n\u251c\u2500\u2500 libs\n\u2502   \u251c\u2500\u2500 cgse-common/\n\u2502   \u251c\u2500\u2500 cgse-coordinates/\n\u2502   \u251c\u2500\u2500 cgse-core/\n\u2502   \u251c\u2500\u2500 cgse-gui/\n\u2502   \u2514\u2500\u2500 index.md\n\u251c\u2500\u2500 projects/\n\u2502   \u251c\u2500\u2500 cgse-tools.md\n\u2502   \u251c\u2500\u2500 symetrie-hexapod.md\n\u2502   \u2514\u2500\u2500 index.md\n\u251c\u2500\u2500 images/\n\u2514\u2500\u2500 roadmap.md\n
"},{"location":"dev_guide/installation/","title":"Installation Guide for Developers","text":""},{"location":"dev_guide/installation/#github","title":"GitHub","text":"

Before starting, make sure you have a fork of the cgse repository. Through this fork (which resides on the GitHub server) you will create pull requests. Install a clone of your fork on your local machine or laptop.

So, when you have created a fork in your GitHub account, clone the repository on your local machine. For the purpose of this guide we will clone the repo in the ~/github/cgse folder. The following commands will create the required folders and clone the repo.

$ mkdir -p ~/github\n$ cd ~/github\n$ git clone git@github.com:IvS-KULeuven/cgse.git\n$ cd ~/github/cgse\n

Now you will have to create a virtual environment and populated it with all the dependencies.

Note

The following three commands will get you going quickly:

$ uv venv --python 3.9.20\n$ uv sync --all-packages\n$ uv run cgse\n\nUsage: cgse [OPTIONS] COMMAND [ARGS]...\n\nThe main cgse command to inspect, configure, monitor the core services and device control \nservers.\n\n\u256d\u2500 Options \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 --install-completion          Install completion for the current shell.                                        \u2502\n\u2502 --show-completion             Show completion for the current shell, to copy it or customize the installation. \u2502\n\u2502 --help                        Show this message and exit.                                                      \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\u256d\u2500 Commands \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 version   Prints the version of the cgse-core and other registered packages.                                   \u2502\n\u2502 top       A top-like interface for core services and device control servers.                                   \u2502\n\u2502 clock     Showcase for running an in-line Textual App.                                                         \u2502\n\u2502 init      Initialize your project.                                                                             \u2502\n\u2502 show      Show information about settings, environment, setup, ...                                             \u2502\n\u2502 check     Check installation, settings, required files, etc.                                                   \u2502\n\u2502 dev-x     device-x is an imaginary device that serves as an example                                            \u2502\n\u2502 core      handle core services: start, stop, status                                                            \u2502\n\u2502 puna      PUNA Positioning Hexapod, Sym\u00e9trie                                                                   \u2502\n\u2502 daq6510   DAQ6510 Data Acquisition Unit, Keithley, temperature monitoring                                      \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n

"},{"location":"dev_guide/monorepo/","title":"The structure of this monorepo","text":"

Currently, the structure starts with two main folders in the root, i.e. libs and projects. Where libs contains library type packages like common modules, small generic gui and tui functions, reference frames, ... and projects contain packages that build upon these libraries and can be device drivers or stand-alone applications.

There is one package that I think doesn't fit into this picture, that is cgse-core. This is not a library, but a \u2013 collection of \u2013 service(s). So, we might want to add a third top-level folder services but I also fear that this again more complicates the monorepo.

Anyway, the overall structure of the monorepo is depicted below:

cgse/\n\u2502\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 libs/\n\u2502   \u251c\u2500\u2500 cgse-common/\n\u2502   \u2502   \u251c\u2500\u2500 src/\n\u2502   \u2502   \u251c\u2500\u2500 tests/\n\u2502   \u2502   \u2514\u2500\u2500 pyproject.toml\n\u2502   \u251c\u2500\u2500 cgse-core/\n\u2502   \u2502   \u251c\u2500\u2500 src/\n\u2502   \u2502   \u251c\u2500\u2500 tests/\n\u2502   \u2502   \u2514\u2500\u2500 pyproject.toml\n\u2502   \u251c\u2500\u2500 cgse-coordinates/\n\u2502   \u2502   \u251c\u2500\u2500 src/\n\u2502   \u2502   \u251c\u2500\u2500 tests/\n\u2502   \u2502   \u2514\u2500\u2500 pyproject.toml\n\u2502   \u2514\u2500\u2500 cgse-gui/\n\u2502   \u2502   \u251c\u2500\u2500 src/\n\u2502   \u2502   \u251c\u2500\u2500 tests/\n\u2502   \u2502   \u2514\u2500\u2500 pyproject.toml\n\u2502\n\u2514\u2500\u2500 projects/\n    \u251c\u2500\u2500 generic/\n    \u2502   \u251c\u2500\u2500 cgse-tools/\n    \u2502   \u251c\u2500\u2500 keithley-tempcontrol/\n    \u2502   \u2514\u2500\u2500 symetrie-hexapod/\n    \u2514\u2500\u2500 plato/\n        \u251c\u2500\u2500 plato-spw/\n        \u251c\u2500\u2500 plato-fits/\n        \u2514\u2500\u2500 plato-hdf5/\n

We will discuss the structure of individual packages in a later section, for now let's look at the root of the monorepo. The root also contains a pyproject.toml file although this is not a package that will be build and published. The purpose of this root pyproject.toml file is to define properties that are used to build the full repo or any individual package in it. In the root folder we will also put some maintenance/management scripts to help you maintain and bump versions of the projects, build and publish all projects, create and maintain a changelog etc.

"},{"location":"dev_guide/monorepo/#package-structure","title":"Package Structure","text":"

We try to keep the package structure as standard as possible and consistent over the whole monorepo. The structure currently is as follows (example from cgse-common):

\u251c\u2500\u2500 README.md\n\u251c\u2500\u2500 pyproject.toml\n\u251c\u2500\u2500 src/\n\u2502   \u2514\u2500\u2500 egse/  # namespace, i.e. there shall not be a __init__.py in this folder\n\u2502       \u251c\u2500\u2500 modules (*.py)\n\u2502       \u2514\u2500\u2500 <sub-packages>/  # these do contain a __init__.py\n\u2514\u2500\u2500 tests/\n    \u251c\u2500\u2500 data\n    \u2514\u2500\u2500 pytest modules (test_*.py)\n

Note that each library or project is a standalone Python package with its own pyproject.toml file, source code and unit tests.

"},{"location":"dev_guide/monorepo/#package-versions","title":"Package versions","text":"

All packages in the monorepo will have the same version. This can be maintained with the bump.py script. This script will read the version from the pyproject.toml file at the root of the monorepo and propagate the version to all libs and projects in the monorepo. Note that you \u2013for now\u2013 will have to update the version number in the pyproject.toml file located at the monorepo root folder manually.

"},{"location":"dev_guide/monorepo/#the-egse-namespace","title":"The egse namespace","text":"

You might have notices that all packages in this monorepo have a src/egse folder in which they maintain their source code, preferably in a sub-package. Note that the egse folder is not a normal Python package but a namespace. There are two important facts you need to remember about namespaces:

  1. A namespace package does not contain an __init__.py module, never, in any of the packages in this or any other repo. If you place an __init__.py module in one of your egse package folders, you will break the namespace and therefore also the external contributions in plugins etc.
  2. A namespace package is spread out over several directories that can reside in different packages as distributed by PyPI.
"},{"location":"dev_guide/monorepo/#egse-versus-cgse","title":"egse versus cgse","text":"

Why is there sometimes egse and sometimes cgse used in documentation, folder names etc.? The acronym EGSE stands for Electric Ground Support Equipment and the CGSE stands for Common-EGSE. So, the latter, CGSE, is what we use for the project name, to emphasise its common purpose as a framework for testing instrumentation and for external packages and device drivers to emphasise that they are intended to be common and work well with the CGSE framework. The egse is what the software is about, the electric ground support equipment, and therefore we use this for the namespace, i.e. the root of the library and projects. Using egse as the namespace also avoid any conflicts with the cgse monorepo name.

"},{"location":"dev_guide/plugins/","title":"Plugins","text":"

The CGSE is designed to be extensible and uses a few plugin mechanisms to extend its functionally with external contributions. Also within the cgse monorepo we use the plugin mechanism at several places. The following entry-points are currently defined:

Each of the entry-points knows how to load a module or object and each entry-point group is connected to a specific action or plugin hook like, e.g. add a command or command group to the cgse app, add package specific settings to the global settings.

"},{"location":"dev_guide/plugins/#version-discovery","title":"Version discovery","text":"

When you write a package that you want to integrate with the CGSE, provide a cgse.version entry-point. The name of the entry-point shall match the package name and is used to read the version from the importlib metadata. The entry-point value is currently not used. The entry-point value can optionally provide additional information about the package, but that is currently not specified.

Add the following to your pyproject.toml file in your project's root folder, replacing package-name with the name of your project. The entry-point value is currently not used, but you want to use a valid format, the value below is always valid.

[project.entry-points.\"cgse.version\"]\npackage-name = 'egse.version:get_version_installed'\n
"},{"location":"dev_guide/plugins/#extending-the-cgse-app","title":"Extending the cgse app","text":""},{"location":"dev_guide/plugins/#add-a-command","title":"Add a Command","text":"

If your package provides specific functionality that can be added as a command or a command group to the cgse app, use the cgse.command entry-point group. Since the cgse app uses the Typer package to build its commandline interface, adding a command is as simple as writing a function. The function will be added to the cgse app using the app.command() function of Typer, making the function a top-level command of the cgse app. The function can be defined as a plain function or with Typer's @app.command decorator.

In the pyproject.toml file of your project, add the following lines to add the CGSE command:

[project.entry-points.\"cgse.command\"]\nname = 'module:object'\n

Where:

As an example, for the cgse-tools package, the init command of the cgse app is listed in the pyproject.toml file as follows:

[project.entry-points.\"cgse.command\"]\ninit = 'cgse_tools.cgse_commands:init'\n

The init function is defined in the cgse_commands.py module which is located in the cgse_tools module in the src folder of the package:

src\n\u251c\u2500\u2500 cgse_tools\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u251c\u2500\u2500 cgse_commands.py\n...\n
"},{"location":"dev_guide/plugins/#add-a-command-group","title":"Add a Command group","text":"

Some commands are more complicated and define a number of sub-commands. An example is the show command where you currently have the sub-commands env and settings

$ cgse show --help\n\n Usage: cgse show [OPTIONS] COMMAND [ARGS]...\n\n Show information about settings, environment, setup, ...\n\n\u256d\u2500 Options \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 --help          Show this message and exit.                                           \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\u256d\u2500 Commands \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 settings   Show the settings that are defined by the installed packages.              \u2502\n\u2502 env        Show the environment variables that are defined for the project.           \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n

The show command is defined as a typer.Typer() object where env and settings are added using the decorator @<app>.command().

import typer\n\nshow = typer.Typer(help=\"Show information about settings, environment, setup, ...\")\n\n\n@show.command(name=\"settings\")\ndef show_settings():\n    ...\n\n\n@show.command(name=\"env\")\ndef show_env():\n    ...\n

To add this command group to the cgse app, the following entry was used in the pyproject. toml file of the cgse-tools project. Notice the [group] at the end of the entry which indicates this is a command group instead of a single command.

[project.entry-points.\"cgse.command\"]\nshow = 'cgse_tools.cgse_commands:show[group]'\n
"},{"location":"dev_guide/plugins/#add-a-service","title":"Add a Service","text":"

If your package provides a device driver or a specific service, use the cgse.service entry-point group. Service entry-points follow the same scheme as command groups, i.e. they are added to the cgse app as a Typer() object. Use the following entry in your pyproject.toml file:

[project.entry-points.\"cgse.service\"]\nname = 'module:object'\n

where:

"},{"location":"dev_guide/plugins/#explore","title":"Explore","text":"

The entry-point cgse.explore can be used to extend functionality without adding a new command or sub-command to the cgse app. The idea is that commands that work on different packages can use this entry-point to perform certain tasks on the package. This is currently used for the show procs command (see below).

The entry-point has the following format:

[project.entry-points.\"cgse.explore\"]\nexplore = \"<package>.cgse_explore\"\n

So, what happens is that a command that wants to apply a functionality on an external package loads the cgse_explore.py module for that package and checks if a function with a specific name exists in that module. It then executes that function. For the show procs command, the function show_processes is expected and it shall return a list of strings which currently are printed to the terminal. This entry-point is currently implemented for cgse-core and cgse-dummy (an external demo package) and when you run the cgse show procs command it looks something like below (the format is from the unix ps -ef command).

\u279c  cgse show procs\n459800007 76849     1   0 11:07PM ttys003    0:03.53 /Users/rik/tmp/test_dummy/venv/bin/python3.9 -m egse.logger.log_cs start\n459800007 76850     1   0 11:07PM ttys003    2:18.60 /Users/rik/tmp/test_dummy/venv/bin/python3.9 -m egse.storage.storage_cs start\n459800007 76851     1   0 11:07PM ttys003    2:20.10 /Users/rik/tmp/test_dummy/venv/bin/python3.9 -m egse.confman.confman_cs start\n459800007 13825     1   0  4:31PM ttys003    0:02.97 /Users/rik/tmp/test_dummy/venv/bin/python3.9 -m cgse_dummy.dummy_sim start\n
"},{"location":"dev_guide/plugins/#register-resources","title":"Register resources","text":"

TODO: what if two packages provide a resource icons ?

"},{"location":"dev_guide/unit_testing/","title":"Testing the Software","text":"

We use the pytest package to unit test our modules and packages. The pyproject.toml files are configured for each package to perform the testing. This section will guide you through the steps to run the tests and also explain how we configured the tests and some guidelines we used.

"},{"location":"dev_guide/unit_testing/#running-the-unit-test-for-each-package-separately","title":"Running the unit test for each package separately","text":"

If you are working on a particular package and want to run its unit test, make sure you are in the root folder of that package, e.g. for the cgse-common package, do the following:

$ cd ~/github/cgse/libs/cgse-common/\n$ uv sync\n$ uv run pytest -v\n================================================================ test session starts =================================================================\nplatform darwin -- Python 3.9.20, pytest-8.3.4, pluggy-1.5.0 -- /Users/rik/github/cgse/.venv/bin/python3\ncachedir: .pytest_cache\nrootdir: /Users/rik/github/cgse/libs/cgse-common\nconfigfile: pyproject.toml\nplugins: cov-6.0.0, mock-3.14.0\ncollected 161 items\n\ntest_bits.py::test_clear_bit PASSED                                                                                                            [  0%]\ntest_bits.py::test_set_bit PASSED                                                                                                              [  1%]\ntest_bits.py::test_toggle_bit PASSED                                                                                                           [  1%]\ntest_bits.py::test_beautify_binary PASSED                                                                                                      [  2%]\ntest_bits.py::test_set_bits PASSED                                                                                                             [  3%]\ntest_bits.py::test_alternative_set_bits PASSED                                                                                                 [  3%]\ntest_bits.py::test_clear_bits PASSED                                                                                                           [  4%]\ntest_bits.py::test_crc_calc PASSED                                                                                                             [  4%]\ntest_bits.py::test_humanize_bytes PASSED                                                                                                       [  5%]\ntest_bits.py::test_s16 PASSED                                                                                                                  [  6%]\ntest_bits.py::test_s32 PASSED                                                                                                                  [  6%]\ntest_command.py::test_dry_run PASSED                                                                                                           [  7%]\ntest_command.py::test_command_class PASSED                                                                                                     [  8%]\ntest_command.py::test_return_code_of_execute PASSED                                                                                            [  8%]\n...\n
"},{"location":"dev_guide/unit_testing/#running-the-unit-tests-of-all-packages","title":"Running the unit tests of all packages","text":"

Before releasing the software, we should run all the unit tests of all the packages in the monorepo and have green light \ud83d\udfe2. Running these unit tests is as simple as for the individual packages. You will need to be in the root folder of the monorepo and sync your virtual environment for all the packages in the workspace.

$ cd ~/gitbug/cgse\n$ uv sync --all-packages\n$ uv run pytest -v\n
"},{"location":"dev_guide/uv/","title":"Working with uv","text":"

uv is an extremely fast Python package and project manager, written in Rust. We will use uv as the single tool that replaces pip, virtualenv, pyenv, and more. The main tasks for which we will use uv are:

"},{"location":"dev_guide/uv/#installing-uv","title":"Installing uv","text":"

On macOS and Linux you can install uv using curl:

$ curl -LsSf https://astral.sh/uv/install.sh | sh\n

If you need more specific information on installing and upgrading uv, please refer to the official documentation.

"},{"location":"dev_guide/uv/#installing-a-python-version","title":"Installing a Python version","text":"

The CGSE is guaranteed to work with Python 3.9.x. We will gradually include higher versions of Python, but currently these have not been tested. So, we will for the moment stick with Python 3.9.20. Install this version as follows:

$ uv python install 3.9.20\n

pyenv

When you are using pyenv to manage your Python versions, make sure you also have the same Python version installed with pyenv and uv. Otherwise you will run into the following error. This is a known issue with uv.

pyenv: version `3.9.20' is not installed (set by /Users/rik/github/cgse/libs/cgse-common/.python-version)\n

You can check which Python versions are installed already on your system:

CommandOutput
$ uv python list --only-installed\n
cpython-3.12.8-macos-aarch64-none     /Users/rik/Library/Application Support/uv/python/cpython-3.12.8-macos-aarch64-none/bin/python3.12\ncpython-3.10.16-macos-aarch64-none    /Users/rik/Library/Application Support/uv/python/cpython-3.10.16-macos-aarch64-none/bin/python3.10\ncpython-3.9.21-macos-aarch64-none     /Users/rik/Library/Application Support/uv/python/cpython-3.9.21-macos-aarch64-none/bin/python3.9\ncpython-3.9.20-macos-aarch64-none     /Users/rik/Library/Application Support/uv/python/cpython-3.9.20-macos-aarch64-none/bin/python3.9\ncpython-3.9.6-macos-aarch64-none      /Library/Developer/CommandLineTools/usr/bin/python3 -> ../../Library/Frameworks/Python3.framework/Versions/3.9/bin/python3\ncpython-3.8.17-macos-aarch64-none     /Users/rik/Library/Application Support/uv/python/cpython-3.8.17-macos-aarch64-none/bin/python3.8\n
"},{"location":"dev_guide/uv/#create-a-virtual-environment","title":"Create a virtual environment","text":"

Pin a Python version

You can pin a python version with the command:

$ uv python pin 3.9.20\n

uv will search for a pinned version in the parent folders up to the root folder or your home directory.

You can create a virtual environment with uv for the specific Python version as follows. The '--python' is optional and uv will use the default (pinned) Python version when creating a venv without this option. We are working in a monorepo or what uv calls a workspace. There will be only one virtual environment at the root of the monorepo, despite the fact that we have several individual packages in our workspace. Don't worry, uv will always use the virtual environment at the root and keep it up-to-date with the project your are currently working in.

When creating a virtual environment make sure you are in the root folder, e.g. ~/github/cgse.

$ cd ~/github/cgse\n$ uv venv --python 3.9.20\n
If you want to name your virtual environment, use the optional argument --prompt <venv name> in the above command, otherwise the virtual environment will get the same name as the project, i.e. cgse.

Now, navigate to the package you will be working in and update the projects' environment, assuming you are going to work in cgse-core, this will be:

$ cd ~/github/cgse/libs/cgse-core\n$ uv sync\n

Your package(s) from the workspace should be installed as an editable install. You can check this with the command:

$ uv pip list -v\nUsing Python 3.9.20 environment at: /Users/rik/github/cgse/.venv\nPackage           Version     Editable project location\n----------------- ----------- ---------------------------------------\napscheduler       3.11.0\ncgse-common       0.4.0       /Users/rik/github/cgse/libs/cgse-common\ncgse-core         0.4.0       /Users/rik/github/cgse/libs/cgse-core\n...\n

To install any other project as an editable package:

$ uv pip install -e <project location>\n

Note

If you don't want to use the uv commands, you can activate the virtual environment and use the original pip and python commands as you are used to, but I would recommend you try to get used to uv for a while to experience its benefits.

$ source ~/github/cgse/.venv/bin/activate\n

Info

In a workspace, maintaining a virtual environment per package might be a hassle and most of the time that is not needed. A good approach is to always use the virtual environment at the workspace root. This venv which will be automatically created if you run a command or if you use uv sync in the package folder. With uv sync you can make sure the virtual environment is up-to-date and contains only those dependencies that are required for the package you are in. So, each time you switch to another package and want to run a comand or a test for that package, use

$ uv sync\n
"},{"location":"dev_guide/uv/#building-and-publishing-all-packages","title":"Building and publishing all packages","text":"

We have chosen for one and the same version number for all packages in the cgse monorepo. That means that whenever we make a change to one of the packages and want to release that change, all packages shall be rebuild and published.

Inline

When working in a workspace, keep in mind that the commands uv run and uv sync by default work on the workspace root. That means that when you run the uv run pip install <package> command, the .venv at the workspace root will be updated or created if it didn't exist. Similar for the uv sync command, there is only one uv.lock file at the root of the workspace.

Fortunately, with uv, that is done in a few commands.

When you are in the monorepo root folder, you can build all packages at once. They will be placed in the dist folder of the root package. Before building, make sure you update the version in the pyproject.toml of the root package and then bump the versions. Before building, clean up the dist folder, then you can do a default uv publish afterwards.

$ cd <monorepo root>\n$ uv run bump.py\n$ rm -r dist\n$ uv build --all-packages\n

Publish all packages in the root dist folder to PyPI. The UV_PUBLISH_TOKEN can be defined in a (read protected) ~/. setenv.bash file:

$ uv publish --token $UV_PUBLISH_TOKEN\n

The above command will publish all package to PyPI. If you don't want the token to be in a shell variable, you can omit the --token in the command above. You will then be asked for a username, use __token__ as the username and then provide the token as a password.

"},{"location":"dev_guide/versioning/","title":"Semantic Versioning","text":"

We use semantic versioning, aka semver, for our releases and patches. Please follow the rules that are described on their site.

TL;DR

The version number has the format MAJOR.MINOR.PATH, we increment the

The rules above apply when MAJOR >= 1, which are considered stable releases.

As long as MAJOR == 0, we are in initial development and anything may change. The MINOR number will be increased for adding or removing functionality and the PATCH number will be increased for all kinds of fixes.

You might occasionally see pre-release and build metadata added to the version number. We will use the following metadata:

"},{"location":"dev_guide/versioning/#why-not-calver","title":"Why not CalVer?","text":"

We do not use Calendar Versioning for the following reason:

"},{"location":"libs/","title":"Libraries","text":"

The libraries are those packages that make up the CGSE framework.

The libraries are located under the libs folder, and we currently find the following packages there:

"},{"location":"libs/cgse-common/","title":"Common Code","text":"

This package cgse-common contains modules that are used by all other packages.

Module Name Description egse.bits convenience functions to work with bits, bytes and integers egse.calibration functions to handle conversions and apply correction egse.command classes and functions to work with commands that operate hardware devices egse.config convenience functions to configure the system and find folders and files egse.control defines abstract classes and convenience functions for any control server egse.decorators a collection of useful decorator functions egse.device defines the generic interfaces to connect devices egse.env functionality to work with and check your environment variables egse.exceptions common Exceptions and Errors egse.hk functions to retrieve and convert housekeping parameter values egse.metrics functions to define and update metrics egse.mixin defines the mixin classes for dynamic commanding egse.monitoring the monitoring application / function egse.observer the classic observer and observable egse.obsid functions to define and work with the OBSID egse.persistence the persistence layer interface egse.plugin functions to load plugins and settings from entry-points egse.process functions and classes to work with processes and sub-processes egse.protocol base class for communicating commands with the hardware or the control server egse.proxy base class for the Proxy objects for each device controller egse.reload a slightly better approach to reloading modules and function egse.resource convenience functions to use resources in your code egse.response defines the classes to handle responses from the control servers egse.services provides the services to the control servers egse.settings provides functions to handle user and configuration settings egse.setup defines the Setup, containing the complete configuration for a test egse.state classes and functions to handle state, e.g. the GlobalState egse.system convenience functions that provide information on system specific functionality egse.version functions to load specific version information egse.zmq_ser serialization function used in a ZeroMQ context"},{"location":"libs/cgse-common/settings/","title":"The Settings","text":"

The Settings class contains all static information needed to configure your system, the environment you are using and the test equipment. The Settings also contain all the IP addresses and port number for all the known devices, together with other static information like the device name, default settings for the device like speed, timeout, delay time, firmware version, etc. We will go into more details about the content later, let\u2019s now first look at the format and usage of the Settings.

"},{"location":"libs/cgse-common/settings/#loading-the-settings","title":"Loading the Settings","text":"

The Settings can be loaded as follows:

>>> from egse.settings import Settings\n>>> settings = Settings.load()\n

The settings object will be a dictionary where the keys are the top-level groups that are defined in the settings for each package. For a system that has only cgse-common and cgse-core installed, the settings will contain something like this:

>>> print(settings)\nSettings\n\u251c\u2500\u2500 PACKAGES\n\u2502   \u251c\u2500\u2500 CGSE_COMMON: Common classes, functions, decorators, etc. for the CGSE\n\u2502   \u2514\u2500\u2500 CGSE_CORE: The core services of the CGSE\n\u251c\u2500\u2500 SITE\n\u2502   \u251c\u2500\u2500 ID: LAB42\n\u2502   \u251c\u2500\u2500 SSH_SERVER: localhost\n\u2502   \u2514\u2500\u2500 SSH_PORT: 22\n\u251c\u2500\u2500 PROCESS\n\u2502   \u2514\u2500\u2500 METRICS_INTERVAL: 10\n\u251c\u2500\u2500 Logging Control Server\n\u2502   \u251c\u2500\u2500 PROTOCOL: tcp\n\u2502   \u251c\u2500\u2500 HOSTNAME: localhost\n\u2502   \u251c\u2500\u2500 LOGGING_PORT: 7000\n\u2502   \u251c\u2500\u2500 COMMANDING_PORT: 7001\n\u2502   \u251c\u2500\u2500 METRICS_PORT: 7003\n\u2502   \u251c\u2500\u2500 MAX_NR_LOG_FILES: 20\n\u2502   \u251c\u2500\u2500 MAX_SIZE_LOG_FILES: 20\n\u2502   \u251c\u2500\u2500 TEXTUALOG_IP_ADDRESS: 127.0.0.1\n\u2502   \u2514\u2500\u2500 TEXTUALOG_LISTENING_PORT: 19996\n\u251c\u2500\u2500 Configuration Manager Control Server\n\u2502   \u251c\u2500\u2500 PROTOCOL: tcp\n\u2502   \u251c\u2500\u2500 HOSTNAME: localhost\n\u2502   \u251c\u2500\u2500 COMMANDING_PORT: 6000\n\u2502   \u251c\u2500\u2500 MONITORING_PORT: 6001\n\u2502   \u251c\u2500\u2500 SERVICE_PORT: 6002\n\u2502   \u251c\u2500\u2500 METRICS_PORT: 6003\n\u2502   \u251c\u2500\u2500 DELAY: 1\n\u2502   \u2514\u2500\u2500 STORAGE_MNEMONIC: CM\n\u2514\u2500\u2500 Storage Control Server\n    \u251c\u2500\u2500 PROTOCOL: tcp\n    \u251c\u2500\u2500 HOSTNAME: localhost\n    \u251c\u2500\u2500 COMMANDING_PORT: 6100\n    \u251c\u2500\u2500 MONITORING_PORT: 6101\n    \u251c\u2500\u2500 SERVICE_PORT: 6102\n    \u251c\u2500\u2500 METRICS_PORT: 6103\n    \u2514\u2500\u2500 DELAY: 1\n

If you only need the settings for a particular component, specify that group's name:

>>> storage_settings = Settings.load(\"Storage Control Server\")\n\n>>> print(storage_settings)\nStorage\nControl\nServer\n\u251c\u2500\u2500 PROTOCOL: tcp\n\u251c\u2500\u2500 HOSTNAME: localhost\n\u251c\u2500\u2500 COMMANDING_PORT: 6100\n\u251c\u2500\u2500 MONITORING_PORT: 6101\n\u251c\u2500\u2500 SERVICE_PORT: 6102\n\u251c\u2500\u2500 METRICS_PORT: 6103\n\u2514\u2500\u2500 DELAY: 1\n

The values can be accessed as usual with a dictionary, by specifying the name of the parameter as the key:

>>> print(storage_settings[\"COMMANDING_PORT\"])\n6100\n

We usually only go one level deep when defining settings, and as a convenience, that first level of variables can also be accessed with the dot-notation.

>>> print(storage_settings.COMMANDING_PORT)\n6100\n
"},{"location":"libs/cgse-common/settings/#entry-points","title":"Entry-points","text":"

The Settings are collected from a set of YAML files which are provided by the packages through the entry-point cgse.settings. The default Settings file is named settings.yaml but this can be changed by the entry-point (see below).

Let's take a look at how the settings are provided for the cgse-core package. First, the pyproject.toml file of the project shall define the entry-point. In the snippet below, the entry-point cgse-core is defined for the group cgse.settings.

[project.entry-points.\"cgse.settings\"]\ncgse-core = \"cgse_core:settings.yaml\"\n

The entry-point itself has the following format: <name> = \"<module>.<filename>\", where

Note

The module name for this entry point has an underscore instead of a dash, i.e. cgse_core instead of cgse-core. The reason is that module names with a dash will generate a SyntaxError during import.

The above example will load the settings for this package from the settings.yaml file that is located in the cgse_core module. That is, the package shall also provide this as follows:

cgse-core\n\u251c\u2500\u2500 pyproject.toml\n\u2514\u2500\u2500 src\n    \u2514\u2500\u2500 cgse_core\n        \u251c\u2500\u2500 __init__.py\n        \u2514\u2500\u2500 settings.yaml\n

The settigs.yaml file for this module looks something like this:

PACKAGES:\n    CGSE_CORE: The core services of the CGSE\n\nLogging Control Server:                          # LOG_CS\n\n    PROTOCOL:                       tcp\n    HOSTNAME:                 localhost          # The hostname that client shall connect to, e.g. pleiad01 @ KU Leuven\n    LOGGING_PORT:                  7000\n    COMMANDING_PORT:               7001\n    METRICS_PORT:                  7003          # The HTTP port where Prometheus will connect to for retrieving metrics\n    MAX_NR_LOG_FILES:                20          # The maximum number of log files that will be maintained in a roll-over\n    MAX_SIZE_LOG_FILES:              20          # The maximum size one log file can become\n    TEXTUALOG_IP_ADDRESS:     127.0.0.1          # The IP address of the textualog listening server\n    TEXTUALOG_LISTENING_PORT:     19996          # The port number on which the textualog server is listening\n\nConfiguration Manager Control Server:            # CM_CS\n\n    ...\n

Warning

Please note that the module where the Settings YAML file resides is a Python package and not a namespace. That means it shall have a __init__.py file as shown in the example of the cgse_core module above.

If the __init__.py file is not there, you will get an error like below:

ERROR:egse.plugin:The entry-point 'cgse-coordinates' is ill defined. The module part doesn't \nexist or is a namespace. No settings are loaded for this entry-point.\n
"},{"location":"libs/cgse-common/settings/#local-settings","title":"Local Settings","text":"

You can, and you should, define local settings for your project and put those settings in a known folder on your system. The usual place is ~/cgse/local-settings.yaml. This file will be automatically loaded by the Settings.load() function when you define the local settings environment variable. That variable name is <PROJECT>_LOCAL_SETTINGS where <PROJECT> is the name of your project as defined by the PROJECT environment variable. For a PROJECT=LAB23 the local settings would be defined as follows:

$ export LAB23_LOCAL_SETTINGS=~/cgse/local-settings-lab23.yaml\n

The local settings take higher precedence that will overwrite the global settings when loaded. You only need to define the settings that actually change for your local installation, respect the full hierarchy when specifying those settings. You are allowed to define new entries at any level in the Settings hierarchy.

The usual parameters to put into a local settings file are:

"},{"location":"libs/cgse-common/settings/#terminal-command","title":"Terminal Command","text":"

You can check the current settings from the terminal with the following command:

$ py -m egse.settings\nSettings\n\u251c\u2500\u2500 PACKAGES\n\u2502   \u251c\u2500\u2500 CGSE_COMMON: Common classes, functions, decorators, etc. for the CGSE\n\u2502   \u2514\u2500\u2500 CGSE_CORE: The core services of the CGSE\n\u251c\u2500\u2500 SITE\n\u2502   \u251c\u2500\u2500 ID: LAB42\n\u2502   \u251c\u2500\u2500 SSH_SERVER: localhost\n\u2502   \u2514\u2500\u2500 SSH_PORT: 22\n\u251c\u2500\u2500 PROCESS\n\u2502   \u2514\u2500\u2500 METRICS_INTERVAL: 10\n\u251c\u2500\u2500 Logging Control Server\n\u2502   \u251c\u2500\u2500 PROTOCOL: tcp\n\u2502   \u251c\u2500\u2500 HOSTNAME: localhost\n\u2502   \u251c\u2500\u2500 LOGGING_PORT: 7000\n\u2502   \u251c\u2500\u2500 COMMANDING_PORT: 7001\n... ...\n\u2514\u2500\u2500 Storage Control Server\n    \u251c\u2500\u2500 PROTOCOL: tcp\n    \u251c\u2500\u2500 HOSTNAME: localhost\n    \u251c\u2500\u2500 COMMANDING_PORT: 6100\n    \u251c\u2500\u2500 MONITORING_PORT: 6101\n    \u251c\u2500\u2500 SERVICE_PORT: 6102\n    \u251c\u2500\u2500 METRICS_PORT: 6103\n    \u2514\u2500\u2500 DELAY: 1\nMemoized locations:\n['/Users/rik/github/cgse/libs/cgse-common/src/cgse_common/settings.yaml', '/Users/rik/github/cgse/libs/cgse-core/src/cgse_core/settings.yaml', '/Users/rik/cgse/local_settings_ariel.yaml']\n

The memoized locations are the settings files that have been loaded and cached. Once the application has started and the settings have been loaded, they can only be reloaded by explicitly forcing a reload as follows:

>>> settings = Settings.load(force=True)\n

Warning

The force reload does however not guarantee that the settings will propagate properly throughout the application or to client apps. Settings can be saved in local variables or class instances that have no knowledge of a Settings reload. So, be careful when changing your Settings. If there are parameters that change often and are not as static as thought, maybe they belong in the Setup instead of the Settings. Examples are:

"},{"location":"libs/cgse-common/settings/#the-design-of-the-load-method","title":"The design of the load() method","text":"

A word about the Settings.load() method. Depending on the parameters provided, this method either loads all settings, a group of settings or just one single YAML file. We have already explained how to load a specific group of settings by giving the name of the group as a parameter. When you want to load just one YAML file, you need to specify its location also. When a location is given as a str or a Path, the Settings will be loaded from that file only, using the default settings.yaml name or another name given through the filename argument.

This can be used to e.g. load command files for a device:

>>> commands = Settings.load(location=\"~\", filename=\"DAQ5610.yaml\")\n

The mechanism behind the Settings.load() method is shown in the following diagram. For simplicity, parameters are not shown and only the success path is presented, not any exceptions or error handling.

"},{"location":"libs/cgse-common/setup/","title":"The Setup","text":""},{"location":"libs/cgse-coordinates/","title":"Reference Coordinates","text":""},{"location":"libs/cgse-core/","title":"Core Services","text":""},{"location":"libs/cgse-gui/","title":"GUI Components","text":""},{"location":"projects/","title":"Projects","text":"

The projects are those packages that add functionality to the CGSE framework.

The projects live under the folder projects, and they are organised in generic and specific projects. Generic projects do not have an implementation that is specific for one particular project, while, obviously, specific projects have. We currently have the following generic packages:

and then there are the project specific packages:

"},{"location":"projects/cgse-tools/","title":"Tools for the CGSE framework","text":""},{"location":"projects/symetrie-hexapod/","title":"The Sym\u00e9trie Hexapods","text":""},{"location":"projects/symetrie-hexapod/#settings-up-your-system-for-the-puna-hexapod","title":"Settings up your system for the PUNA Hexapod","text":"

Warning

We need some work here... we want to be able to use multiple hexapods in the same Setup and they can be the same type or different types. So, how do we specify two PUNA hexapods used to position two different parts of your test equiopment?

The system needs to know the following information on the hexapod:

These above settings can olso be specified in the environment variables:

"},{"location":"user_guide/","title":"User Guide","text":"

Welcome to the CGSE user guide! An in-depth reference on how to use the CGSE.

"}]} \ No newline at end of file