diff --git a/README.md b/README.md index c914d47..6840cb8 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,24 @@ Other manuals are created with the same commands. Now move the HTML files for each of the manuals to the `docs/asciidocs` folder so they become part of the GitHub pages. -The site also provides the API documentation of the Common-EGSE. This documentation shall be generated in the `plato-common-egse` repository. Go into the `MKDOCS` folder and run the following command: +The site also provides the API documentation of the Common-EGSE. This documentation shall be generated in the `plato-common-egse` repository. Go into the root folder of the project and run the following command: ``` $ pdoc3 --html --output-dir api egse --force +``` +or +``` $ pdoc3 --html --output-dir ~/Documents/PyCharmProjects/plato-cgse-doc/docs/api egse --force ``` The move the `api` folder from the `plato-common-egse` repo into the `docs` folder of this repo. +If you need to exclude certain modules from the API documentation, create the `__pdoc__` variable in the __init__.py of that module. As an example, exclude the `eksma` module from the `egse.filterwheel`, add the following lines in the `__init__.py` of the `egse.filterwheel` module. + +``` +__pdoc__ = { + 'eksma': False +} +``` + You are now ready to commit and push the updated documentation or to make a pull request. The site will automatically be uploaded to the GitHub pages and be available at [github.io](https://ivs-kuleuven.github.io/plato-cgse-doc/). If you are using PyCharm, updating this documentation is easiest if you install the `Asciidoc` plugin in PyCharm. That will allow you to generate the HTML and PDF documents with just one click. @@ -47,3 +58,27 @@ Where do we need to update versions before pushing changes: * In each of the main files: icd.adoc, installation-manual.adoc, developer-manual.adoc, and user-manual.adoc * In the `changelog.adoc` file for each of the manuals * In the HUGO content `posts` and /or `docs` for those manuals that were updated. + +## Installation of AsciiDoctor on macOS + +Make sure you have the `ruby` command from brew in your path instead of the system provided version. + +See https://docs.asciidoctor.org/asciidoctor/latest/install/ +``` +$ brew install asciidoctor +``` + +Then, install asciidoctor-pdf, see https://github.com/asciidoctor/asciidoctor-pdf +``` +$ gem install asciidoctor-pdf +``` + +Then, install asciidoctor-tabs, see https://github.com/asciidoctor/asciidoctor-tabs +``` +gem install [--prerelease] asciidoctor-tabs +``` + +Install HUGO: +``` +$ brew install hugo +``` diff --git a/docs/404.html b/docs/404.html index 4c978a3..4229493 100644 --- a/docs/404.html +++ b/docs/404.html @@ -7,7 +7,7 @@ The Common-EGSE Documentation - + @@ -33,9 +33,9 @@ + - - + @@ -163,7 +163,7 @@

Module egse.coordinates.laser_tracker_to_dictModule egse.coordinates.laser_tracker_to_dictModule egse.coordinates.laser_tracker_to_dictFunctions
-def laser_tracker_to_dict(filexls, setup) +def laser_tracker_to_dict(filexls, setup: Setup)

laser_tracker_to_dict(filexls)

@@ -173,7 +175,7 @@

Functions

hardcoding them would cause trouble when ingesting a partial model - the ReferenceFrames references are included, but they are based on a hardcoded model. -In particular it is assumed that gliso is the master!

+In particular, it is assumed that gliso is the master!

- a "Master" ReferenceFrame is enforced, with the name "Master" (capital)
 
 - the names of the reference frames are returned lowercase, without '_'
@@ -183,7 +185,7 @@ 

Functions

Expand source code -
def laser_tracker_to_dict(filexls, setup):
+
def laser_tracker_to_dict(filexls, setup: Setup):
     """
     laser_tracker_to_dict(filexls)
 
@@ -201,7 +203,7 @@ 

Functions

hardcoding them would cause trouble when ingesting a partial model - the ReferenceFrames references are included, but they are based on a hardcoded model. - In particular it is assumed that gliso is the master! + In particular, it is assumed that gliso is the master! - a "Master" ReferenceFrame is enforced, with the name "Master" (capital) @@ -209,9 +211,9 @@

Functions

("Master" is an exception) """ - + # Predefined model -- gliso ~ master - + """ predef_refs={} predef_refs['gltab'] = 'glfix' @@ -239,7 +241,7 @@

Functions

predef_refs['cambor'] = 'toualn' """ - predef_refs=setup.csl_model.default_refs + predef_refs = setup.csl_model.default_refs # Read input file @@ -251,39 +253,39 @@

Functions

colx = pan["x"].values coly = pan["y"].values colz = pan["z"].values - - refFrames = {} + + refFrames = dict() refFrames["Master"] = 'ReferenceFrame//([0.0000,0.0000,0.0000 | [0.0000,0.0000,0.0000 | Master | Master | [])' - + links = '[]' - - i,frame = -1,-1 - while (i<nrows): + + i, frame = -1, -1 + while i < nrows: i += 1 - + try: frame = desc[i].find("Frame") except: frame = -1 continue - - if (frame >= 0): - + + if frame >= 0: + try: - name = desc[i][desc[i].find("::")+2:].lower().replace("_","") - - if (desc[i+2].lower().find("translation")<0) or (desc[i+3].lower().find('rotation')<0): - raise Exception(f"Unexpected File Structure after row {i} : {desc[i]}") - - translation = f"[{float(colx[i+2]):.6f},{float(coly[i+2]):.6f},{float(colz[i+2]):.6f}" - rotation = f"[{float(colx[i+3]):.6f},{float(coly[i+3]):.6f},{float(colz[i+3]):.6f}" - - if name in predef_refs.keys(): + name = desc[i][desc[i].find("::")+2:].lower().replace("_", "") + + if (desc[i+2].lower().find("translation")<0) or (desc[i+3].lower().find('rotation') < 0): + raise Exception(f"Unexpected File Structure after row {i} : {desc[i]}") + + translation = f"[{float(colx[i+2]):.6f},{float(coly[i+2]):.6f},{float(colz[i+2]):.6f}" + rotation = f"[{float(colx[i+3]):.6f},{float(coly[i+3]):.6f},{float(colz[i+3]):.6f}" + + if name in predef_refs.keys(): ref = predef_refs[name] - else: + else: ref = 'None' - refFrames[name] = f"ReferenceFrame//({translation} | {rotation} | {name} | {ref} | {links})" + refFrames[name] = f"ReferenceFrame//({translation} | {rotation} | {name} | {ref} | {links})" except: print(f"Frame extraction issue after row {i} : {desc[i]}") diff --git a/docs/api/egse/coordinates/pyplot.html b/docs/api/egse/coordinates/pyplot.html index b5ae2fe..214b135 100644 --- a/docs/api/egse/coordinates/pyplot.html +++ b/docs/api/egse/coordinates/pyplot.html @@ -168,7 +168,6 @@

Module egse.coordinates.pyplot

Use ax.set_xlim3d(min,max) to properly set the ranges of the display. """ - #from egse.coordinates.point import Points if master is None: tmpmaster = ReferenceFrame.createMaster() @@ -203,16 +202,16 @@

Module egse.coordinates.pyplot

y = np.ones_like(xs) * y z = np.ones_like(xs) * z - ### PLOT + # PLOT fig = plt.figure(figname) ax = fig.gca(projection='3d') - if fromorigin == True: + if fromorigin: ax.quiver(x, y, z, xs-x, ys-y, zs-z, **kwargs) - elif fromorigin == False: + elif not fromorigin: ax.quiver(xs, ys, zs, x-xs, y-ys, z-zs, **kwargs) @@ -437,7 +436,6 @@

Returns

Use ax.set_xlim3d(min,max) to properly set the ranges of the display. """ - #from egse.coordinates.point import Points if master is None: tmpmaster = ReferenceFrame.createMaster() @@ -472,16 +470,16 @@

Returns

y = np.ones_like(xs) * y z = np.ones_like(xs) * z - ### PLOT + # PLOT fig = plt.figure(figname) ax = fig.gca(projection='3d') - if fromorigin == True: + if fromorigin: ax.quiver(x, y, z, xs-x, ys-y, zs-z, **kwargs) - elif fromorigin == False: + elif not fromorigin: ax.quiver(xs, ys, zs, x-xs, y-ys, z-zs, **kwargs) diff --git a/docs/api/egse/das.html b/docs/api/egse/das.html index f970f33..49ff8a7 100644 --- a/docs/api/egse/das.html +++ b/docs/api/egse/das.html @@ -92,39 +92,40 @@

Module egse.das

``` """ -import itertools import logging import multiprocessing -from datetime import timezone - -import rich - -from egse.hk import read_conversion_dict -from egse.state import GlobalState - -multiprocessing.current_process().name = "das" - import re -import sys -import time from datetime import datetime +from datetime import timezone from pathlib import Path from typing import List import click import invoke -from prometheus_client import Gauge, Summary +import itertools +import rich +import sys +import time +from prometheus_client import Gauge from prometheus_client import start_http_server -from egse.confman import ConfigurationManagerProxy +from egse.aeu.aeu import CRIOProxy, OperatingMode +from egse.aeu.aeu import is_aeu_cs_active from egse.control import Failure +from egse.hk import read_conversion_dict, convert_hk_names +from egse.metrics import define_metrics +from egse.ni.alarms.cdaq9375 import cdaq9375Proxy +from egse.ni.alarms.cdaq9375_cs import is_cdaq9375_cs_active +from egse.powermeter.ni.cdaq9184 import cdaq9184Proxy +from egse.powermeter.ni.cdaq9184_cs import is_cdaq9184_cs_active from egse.settings import Settings -from egse.setup import Setup +from egse.setup import Setup, load_setup from egse.storage import StorageProxy from egse.storage import is_storage_manager_active from egse.storage.persistence import CSV -from egse.system import SignalCatcher, format_datetime +from egse.system import SignalCatcher from egse.system import flatten_dict +from egse.system import format_datetime from egse.tcs.tcs import TCSProxy from egse.tcs.tcs import is_tcs_cs_active from egse.tempcontrol.keithley.daq6510 import DAQ6510Proxy @@ -134,14 +135,6 @@

Module egse.das

from egse.tempcontrol.srs.ptc10 import ptc10Proxy from egse.tempcontrol.srs.ptc10_cs import is_ptc10_cs_active -from egse.powermeter.ni.cdaq9184 import cdaq9184Proxy -from egse.powermeter.ni.cdaq9184_cs import is_cdaq9184_cs_active - -from egse.metrics import define_metrics -from egse.synoptics import SynopticsManagerProxy - -from egse.system import format_datetime - LOGGER = logging.getLogger(__name__) DAS = Settings.load("Data Acquisition System") @@ -158,12 +151,6 @@

Module egse.das

return Setup.from_yaml_file(input_file) -def load_setup_from_configuration_manager(): - """Loads a Setup YAML file from the Configuration Manager.""" - - return GlobalState.setup - - class Config: def __init__(self): self.verbose = False @@ -217,7 +204,16 @@

Module egse.das

""" - hk_conversion_table = read_conversion_dict("DAS-DAQ6510", use_site=True) + if input_file: + setup = load_setup_from_input_file(input_file) + else: + setup = load_setup() + + if setup is None: + LOGGER.error("ERROR: Could not load setup.") + sys.exit(1) + + hk_conversion_table = read_conversion_dict("DAS-DAQ6510", use_site=True, setup=setup) column_names = list(hk_conversion_table.values()) if background: @@ -246,15 +242,6 @@

Module egse.das

"before running the data acquisition.") return - if input_file: - setup = load_setup_from_input_file(input_file) - else: - setup = load_setup_from_configuration_manager() - - if setup is None: - LOGGER.error("ERROR: Could not load setup.") - sys.exit(1) - if config.verbose: LOGGER.info(setup) @@ -295,7 +282,7 @@

Module egse.das

prep = { "mode": "a", "ending": "\n", - "column_names": ["Timestamp", *column_names], + "column_names": ["timestamp", *column_names], } killer = SignalCatcher() @@ -359,11 +346,12 @@

Module egse.das

# LOGGER.debug(f"{response=}") dts = response[0][1].strip() - datetime_string = format_datetime(datetime.strptime(dts[:-3], "%m/%d/%Y %H:%M:%S.%f")) + dt = datetime.strptime(dts[:-3], "%m/%d/%Y %H:%M:%S.%f") + datetime_string = format_datetime(dt.replace(tzinfo=timezone.utc)) data = {hk_conversion_table[measure[0]]: float(measure[2]) for measure in response} - data.update({"Timestamp": datetime_string}) + data.update({"timestamp": datetime_string}) # FIXME: we probably need to do something with the units... @@ -427,8 +415,8 @@

Module egse.das

if background: cmd = "das ptc10" - cm += f"--user_regulation {user_regulation}" - cm += f"--auto_regulation {auto_regulation}" + cmd += f" --user_regulation {user_regulation}" + cmd += f" --auto_regulation {auto_regulation}" cmd += f" {input_file}" if input_file else "" LOGGER.info(f"Invoking background command: {cmd}") invoke.run(cmd, disown=True) @@ -452,7 +440,7 @@

Module egse.das

if input_file: setup = load_setup_from_input_file(input_file) else: - setup = load_setup_from_configuration_manager() + setup = load_setup() if setup is None: LOGGER.error("ERROR: Could not load setup.") @@ -478,7 +466,7 @@

Module egse.das

# HK_names is ['GIAS_TTS_BiP_01', 'GIAS_TRP2', 'GIAS_TRP3', 'GIAS_TRP4', 'GIAS_H1_ampere', 'GIAS_H2_ampere', 'GIAS_H3_ampere', 'GIAS_H1_watt', 'GIAS_H2_watt', 'GIAS_H3_watt', 'GIAS_H1_volt', 'GIAS_H2_volt', 'GIAS_H3_volt'] # Creation of Prometheus METRICS in a dictionnary from TM dictionnary - PTC_METRICS = define_metrics("DAS-PTC10") + PTC_METRICS = define_metrics("DAS-PTC10", setup=setup) if not list(PTC_METRICS.keys()) == HK_names: # Check if names in setup file (used for HK names) and names defined in TM dictionary for metrics names are same LOGGER.error("The names of HK defined in the current setup file " + setup.get_id() + " are not the same than those in TM dictionary for metrics") @@ -492,7 +480,8 @@

Module egse.das

prep = { "mode": "a", "ending": "\n", - "column_names": ["Timestamp", *HK_names], + "header": "PTC10 First Connection Tests", + "column_names": ["timestamp", *PTC_METRICS], } killer = SignalCatcher() @@ -500,7 +489,6 @@

Module egse.das

with ptc10Proxy() as ptc, StorageProxy() as storage: storage.register({"origin": origin, "persistence_class": persistence_class, "prep": prep}) - storage.save({"origin": origin, "data": prep["column_names"]}) # Renaming the names of channels in PTC10 device with names defined in the setup file old_channel_names = ptc.get_names() # old_channel_names is a tuple of 2 lists with the names of inputs (4 sensors) @@ -566,7 +554,7 @@

Module egse.das

try: response = [ptc.get_time()] + ptc.read_temperature() + ptc.read_heater()[0] # Sometimes randomly PTC10 doesn't return the time and there is a Failure, so to avoid - # a bad type in the column Timestamp, if there is a Failure, HK are not saved for this + # a bad type in the column timestamp, if there is a Failure, HK are not saved for this # time and the code go back at the top of the while loop thanks to exceptions. except TypeError: LOGGER.warning("TYPE ERROR") @@ -603,21 +591,20 @@

Module egse.das

for i in [5, 6, 7]: response.append(R * response[i]) - # Creation of the dictionary data wich contains only values (not timestamp) for Prometheus metrics - data = {HK_names[i]: response[i+1] for i in range(len(HK_names))} # the first element of response is timestamp which doesn't go into the dictionary data + hk_dict = {"timestamp": response[0]} + hk_dict.update({k: v for k, v in zip(HK_names, response[1:])}) LOGGER.debug(f"{response=}") - LOGGER.debug(data) + LOGGER.debug(hk_dict) - # Saving HK with PTC10 time as timestamp (the response list contains timestamp and values) - storage.save({"origin": origin, "data": response}) - synoptics = SynopticsManagerProxy() - synoptics.store_th_synoptics(response) + # Saving HK with PTC10 time as timestamp + storage.save({"origin": origin, "data": hk_dict}) - # Now extract from data the values measured to update the metrics - for name in PTC_METRICS: - PTC_METRICS[name].set(data[name]) + # Now set the values in the metrics + hk_dict.pop("timestamp") + for key, value in hk_dict.items(): + PTC_METRICS[key].set(value) except KeyboardInterrupt: @@ -702,7 +689,7 @@

Module egse.das

names = patterns.keys() - # For each of the names, create also a Timestamp column for that name + # For each of the names, create also a timestamp column for that name columns = list(itertools.chain.from_iterable((x + '_ts', x) for x in names)) @@ -952,15 +939,17 @@

Module egse.das

""" + setup = load_setup() + if background: - cmd = "das cdaq" + cmd = "das cdaq-photo" cmd += " --use-all-hk" if use_all_hk else "" cmd += f" --interval {interval}" LOGGER.info(f"Invoking background command: {cmd}") invoke.run(cmd, disown=True) return - multiprocessing.current_process().name = "das-cdaq" + multiprocessing.current_process().name = "das-cdaq-photodiodes" if config.debug: logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL) @@ -975,29 +964,24 @@

Module egse.das

"before running the data acquisition.") return - # Channel names for tall the HK, Here we only take the mean values (dismissing stddev values) + metrics_cdaq = define_metrics("DAS-CDAQ-PHOTODIODES", setup=setup) + hk_names = ["GIAS_OGSE2_PHOTOD_1", "GIAS_OGSE2_PHOTOD_2", "GIAS_OGSE2_TAMPLI_1", "GIAS_OGSE2_TAMPLI_2", + "GIAS_OGSE2_TSPHERE", "GIAS_OGSE2_GAMPLI_1", "GIAS_OGSE2_GAMPLI_2"] - channel_names = ["Photodiode_1", "Photodiode_2", "Collimator_Temp_1", "Collimator_Temp_2", "Sphere_Temp"] - - # GRAFANA/PROMETHEUS METRICS - DAQ_METRICS = {} - for channel in channel_names: - DAQ_METRICS[channel] = Gauge(f"cdaq_{channel}", - f"The current measure for the sensor connected to channel {channel} on the CDAQ") + # The unit for photodiodes is V, for temperatures is °C and without unit for gains start_http_server(DAS.METRICS_PORT_CDAQ) # Initialize some variables that will be used for registration to the Storage Manager - # the channel name units for the photodiodes are V, and C for the temperature sensors - origin = "DAS-CDAQ" + origin = "DAS-CDAQ-PHOTODIODES" persistence_class = CSV prep = { "mode": "a", "ending": "\n", "header": "CDAQ First Connection Tests", - "column_names": ["Daytime", "Timestamp", *channel_names], + "column_names": ["timestamp", *metrics_cdaq], } killer = SignalCatcher() @@ -1011,10 +995,6 @@

Module egse.das

while True: try: response = cdaq.read_values() - # Here we only take the mean values (dismissing stddev values) - values = [v for i, v in enumerate(response[2:]) if i % 2 == 0] - del response[2:] - response = response + values if killer.term_signal_received: break @@ -1030,15 +1010,17 @@

Module egse.das

time.sleep(1.0) continue - LOGGER.info(f"Response: {response=}") - storage.save({"origin": origin, "data": response}) - -#demo + reformatted_response = reformat_cdaq_values(response) + values = reformatted_response[2:] + hk_dict = {key: value for key, value in zip(hk_names, values)} + timestamp = format_datetime(datetime.strptime(reformatted_response[0] + '-' + reformatted_response[1], "%y/%m/%d-%H:%M:%S.%f")) + data = [timestamp] + values + storage.save({"origin": origin, "data": data}) - for i in range(len(channel_names)): - DAQ_METRICS[channel_names[i]].set(values[i]) + for key, value in hk_dict.items(): + metrics_cdaq[key].set(value) - time.sleep(interval) + # time.sleep(interval) except KeyboardInterrupt: LOGGER.debug("Interrupt received, terminating...") @@ -1054,6 +1036,263 @@

Module egse.das

storage.unregister({"origin": origin}) +def reformat_cdaq_values(data_to_be_reformatted: list) -> list: + data = data_to_be_reformatted + if len(data) != 35: + LOGGER.error(f"Data received from Labview has not the expected length ({len(data)} but expected 35).") + else: + # The next elements (see the names in the headers) are not wanted so I delete them: + # 1) Filtre_Roue_1, Filtre_Roue_2, Shutter, FEMTO_1_BIAS, FEMTO_2_BIAS, FEMTO_1_Input_Current, + # FEMTO_2_Input_Current (the 7 last elements) + # 2) Temp_4 to Temp_8 (elements from index 16 to 25) + # 3) FEMTO_1_OVERLOAD and FEMTO_2_OVERLOAD (elements at indexes 4, 5, 8 and 9) + + # 1) + del data[-7:] + # 2) + del data[16:26] + # 3) + for i in [9, 8, 5, 4]: + del data[i] + + # Only mean is wanted, not deviation. So I delete all deviation elements + # which are at indexes [3, 5, 7, 9, 11 ( = list(range(3, 12, 2)) ) + for index_to_delete in sorted(list(range(3, 12, 2)), reverse=True): # I delete in the reverse order + del data[index_to_delete] + + # The 2 first elements of data are date as str type. I leave them as str type. I convert the other elements of + # data (which are relevant values) from str to float. + data = data[:2] + [float(value) for value in data[2:]] + + return data + + +@cli.command() +@click.option( + "--background/--no-background", "-bg/-no-bg", default=False, + help="start the data acquisition in the background" +) +@pass_config +def cdaq_alarms(config, background): + """ + Run the Data Acquisition System for the CDAQ alarms (CDAQ9375). + + INPUT_FILE: YAML file containing the Setup for the CDAQ [optional] + + Note: When this command runs in the background, send an INTERRUPT SIGNAL with the kill command + to terminate. Never send a KILL SIGNAL (9) because then the process will not properly be + unregistered from the storage manager. + + $ kill -INT <PID> + + """ + + setup = load_setup() + + if background: + cmd = "das cdaq-alarms" + LOGGER.info(f"Invoking background command: {cmd}") + invoke.run(cmd, disown=True) + return + + multiprocessing.current_process().name = "das-cdaq-alarms" + + if config.debug: + logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL) + + if not is_cdaq9375_cs_active(): + LOGGER.error("The cdaq9375 Control Server is not running, start the 'cdaq9375_cs' command " + "before running the data acquisition.") + return + + if not is_tcs_cs_active(): + LOGGER.error("The TCS Control Server is not running, start the 'tcs_cs' command " + "before running the data acquisition.") + return + + if not is_aeu_cs_active(name="CRIO", timeout=1): + LOGGER.error("The AEU Control Server is not running, start the 'aeu_cs' command " + "before running the data acquisition.") + return + + if not is_storage_manager_active(): + LOGGER.error("The storage manager is not running, start the core services " + "before running the data acquisition.") + return + + metrics_cdaq_alarms = define_metrics("DAS-CDAQ-ALARMS", setup=setup) + + start_http_server(DAS.METRICS_PORT_CDAQ_ALARMS) + + # Initialize some variables that will be used for registration to the Storage Manager + + origin = "DAS-CDAQ-ALARMS" + persistence_class = CSV + prep = { + "mode": "a", + "ending": "\n", + "header": "CDAQ-ALARMS First Connection Tests", + "column_names": ["timestamp", *metrics_cdaq_alarms], + } + + killer = SignalCatcher() + + with cdaq9375Proxy() as cdaq, StorageProxy() as storage, TCSProxy() as tcs_proxy, CRIOProxy() as aeu_crio: + + # Use the names in the header of the CSV file as column names. + + storage.register({"origin": origin, "persistence_class": persistence_class, "prep": prep}) + + alarms_temperature = setup.gse.CDAQ_alarms.alarms_temperature + + trp1_min_op = alarms_temperature.trp1_min_op + trp1_max_op = alarms_temperature.trp1_max_op + trp1_min_nop = alarms_temperature.trp1_min_nop + trp1_max_nop = alarms_temperature.trp1_max_nop + trp22_min_op = alarms_temperature.trp22_min_op + trp22_max_op = alarms_temperature.trp22_max_op + trp22_min_nop = alarms_temperature.trp22_min_nop + trp22_max_nop = alarms_temperature.trp22_max_nop + + alarm_exp1 = False # Low Temp NOP + alarm_exp2 = False # High Temp NOP + alarm_exp3 = False # low/High Temp OP + alarm_exp4 = False # UPS alarm (UPS_Arrakis_alarm_summary or UPS_Ix_alarm_summary) + c3 = 0 # Counter number triggers alarm EXP3 + + while True: + try: + response = cdaq.get_tvac_and_ups_state() + + if killer.term_signal_received: + break + if not response: + LOGGER.warning("Received an empty response from the CDAQ9375, " + "check the connection with the device.") + LOGGER.warning(f"Response: {response=}") + time.sleep(1.0) + continue + if isinstance(response, Failure): + LOGGER.warning(f"Received a Failure from the CDAQ9375 Control Server:") + LOGGER.warning(f"Response: {response}") + time.sleep(1.0) + continue + + # EXP4 + if any([response["UPS_Ix_alarm_summary"], response["UPS_Ix_power_supply_absence"], + response["UPS_Arrakis_alarm_summary"], response["UPS_Arrakis_power_supply_absence"]]): + alarm_exp4 = True + else: + alarm_exp4 = False + + try: + trp1_avg = tcs_proxy.get_housekeeping_value("tou_rtd_tav").value + + if not trp1_avg: + LOGGER.warning("Received an empty response from the TCS, " + "check the connection with the device.") + LOGGER.warning(f"Response: {trp1_avg=}") + time.sleep(1.0) + continue + if isinstance(trp1_avg, Failure): + LOGGER.warning(f"Received a Failure from the TCS EGSE Control Server:") + LOGGER.warning(f"Response: {trp1_avg}") + time.sleep(1.0) + continue + + trp1_avg = float(trp1_avg) + + except ValueError: + LOGGER.warning(f"TRP1 Value Error in cdaq alarms: trp1_avg should be a number, got {trp1_avg}." + f"\nTerminating...") + if trp1_avg == "tbd": + LOGGER.warning("Got TBD for TRP1_AVG.\nCheck if the task is well running.\nTerminating...") + break + + try: + trp22_avg = tcs_proxy.get_housekeeping_value("fee_rtd_tav").value + + if not trp22_avg: + LOGGER.warning("Received an empty response from the TCS, " + "check the connection with the device.") + LOGGER.warning(f"Response: {trp22_avg=}") + time.sleep(1.0) + continue + if isinstance(trp22_avg, Failure): + LOGGER.warning(f"Received a Failure from the TCS EGSE Control Server:") + LOGGER.warning(f"Response: {trp22_avg}") + time.sleep(1.0) + continue + + trp22_avg = float(trp22_avg) + + except ValueError: + LOGGER.warning(f"TRP22_AVG ValueError: trp22_avg should be a number, got {trp22_avg}." + f"\nTerminating...") + if trp22_avg == "tbd": + LOGGER.warning("Got TBD for TRP22_AVG.\nCheck if the task is well running.\nTerminating...") + break + + + aeu_standby = aeu_crio.get_operating_mode() == OperatingMode.STANDBY # True means aeu is in standby mode + + if aeu_standby: + alarm_exp3 = False + c3 = 0 + + + # EXP1 + if trp1_avg < trp1_min_nop or trp22_avg < trp22_min_nop: + alarm_exp1 = True + else: + alarm_exp1 = False + + # EXP2 + if trp1_avg > trp1_max_nop or trp22_avg > trp22_max_nop: + alarm_exp2 = True + else: + alarm_exp2 = False + + # EXP3 + else: + if any([trp1_avg < trp1_min_op, trp1_avg > trp1_max_op, + trp22_avg < trp22_min_op, trp22_avg > trp22_max_op]): + alarm_exp3 = True + c3 += 1 + else: + alarm_exp3 = False + c3 = 0 + + alarm_exp_dict = {"alarm_EXP1": int(alarm_exp1), + "alarm_EXP2": int(alarm_exp2), + "alarm_EXP3": int(alarm_exp3), + "alarm_EXP4": int(alarm_exp4)} + response.update(alarm_exp_dict) + hk_conversion_table = read_conversion_dict("DAS-CDAQ-ALARMS", setup=setup) + hk_dict = convert_hk_names(response, hk_conversion_table) + storage.save({"origin": origin, "data": hk_dict}) + + hk_dict.pop("timestamp") + for key, value in hk_dict.items(): + metrics_cdaq_alarms[key].set(value) + + alarm_exp3 = c3 > 3 # Send SMS via TVAC only if EXP3 is triggered more than 3 consecutive times + cdaq.send_egse_state_to_tvac(alarm_exp1, alarm_exp2, alarm_exp3, alarm_exp4) + + except KeyboardInterrupt: + LOGGER.debug("Interrupt received, terminating...") + break + + except Exception as exc: + LOGGER.warning(f"DAS Exception: {exc}", exc_info=True) + LOGGER.warning("Got a corrupt hk_dict from the CDAQ. " + "Check log messages for 'DAS Exception'.") + time.sleep(1.0) + continue + + storage.unregister({"origin": origin}) + + if __name__ == "__main__": cli()
@@ -1103,21 +1342,6 @@

Args

return value
-
-def load_setup_from_configuration_manager() -
-
-

Loads a Setup YAML file from the Configuration Manager.

-
- -Expand source code - -
def load_setup_from_configuration_manager():
-    """Loads a Setup YAML file from the Configuration Manager."""
-
-    return GlobalState.setup
-
-
def load_setup_from_input_file(input_file: str)
@@ -1207,6 +1431,46 @@

Returns

return processed_data
+
+def reformat_cdaq_values(data_to_be_reformatted: list) ‑> list +
+
+
+
+ +Expand source code + +
def reformat_cdaq_values(data_to_be_reformatted: list) -> list:
+    data = data_to_be_reformatted
+    if len(data) != 35:
+        LOGGER.error(f"Data received from Labview has not the expected length ({len(data)} but expected 35).")
+    else:
+        # The next elements (see the names in the headers) are not wanted so I delete them:
+        # 1) Filtre_Roue_1, Filtre_Roue_2, Shutter, FEMTO_1_BIAS, FEMTO_2_BIAS, FEMTO_1_Input_Current,
+        # FEMTO_2_Input_Current (the 7 last elements)
+        # 2) Temp_4 to Temp_8 (elements from index 16 to 25)
+        # 3) FEMTO_1_OVERLOAD and FEMTO_2_OVERLOAD (elements at indexes 4, 5, 8 and 9)
+
+        # 1)
+        del data[-7:]
+        # 2)
+        del data[16:26]
+        # 3)
+        for i in [9, 8, 5, 4]:
+            del data[i]
+
+        # Only mean is wanted, not deviation. So I delete all deviation elements
+        # which are at indexes [3, 5, 7, 9, 11 ( = list(range(3, 12, 2)) )
+        for index_to_delete in sorted(list(range(3, 12, 2)), reverse=True):  # I delete in the reverse order
+            del data[index_to_delete]
+
+        # The 2 first elements of data are date as str type. I leave them as str type. I convert the other elements of
+        # data (which are relevant values) from str to float.
+        data = data[:2] + [float(value) for value in data[2:]]
+
+        return data
+
+
@@ -1244,9 +1508,9 @@

Index

  • Functions

  • Classes

    diff --git a/docs/api/egse/decorators.html b/docs/api/egse/decorators.html index be39d30..fd31bb4 100644 --- a/docs/api/egse/decorators.html +++ b/docs/api/egse/decorators.html @@ -35,8 +35,10 @@

    Module egse.decorators

    import logging import pstats import time +import types import warnings from typing import Callable +from typing import List from typing import Optional from egse.settings import Settings @@ -46,7 +48,16 @@

    Module egse.decorators

    def static_vars(**kwargs): - """Define static variables in a function.""" + """ + Define static variables in a function. + + The static variable can be accessed with <function name>.<variable name> inside the function body. + + @static_vars(count=0) + def special_count(): + return special_count.count += 2 + + """ def decorator(func): for kw in kwargs: setattr(func, kw, kwargs[kw]) @@ -280,6 +291,36 @@

    Module egse.decorators

    return wrapper_profile +class Profiler: + + @classmethod + def count(cls): + return CountCalls + + +class CountCalls: + def __init__(self, func): + self.func = func + self.count = 0 + + def __call__(self, *args, **kwargs): + self.count += 1 + return self.func(*args, **kwargs) + + def get_count(self): + return self.count + + # The __get__ method is here to make the decorator work with instance methods (methods inside a class) + # as well. It ensures that when the decorated method is called on an instance, the self argument is + # correctly passed to the method. + + def __get__(self, instance, owner): + if instance is None: + return self + else: + return types.MethodType(self, instance) + + def to_be_implemented(func): """Print a warning message that this function/method has to be implemented.""" @@ -441,7 +482,65 @@

    Module egse.decorators

    class_name = obj.__class__.__name__ obj.__class__ = Wrapper - obj.__class__.__name__ = class_name
    + obj.__class__.__name__ = class_name + + +def retry(times: int = 3, wait: float = 10.0, exceptions: List = None): + """ + 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. + + Args: + times (int, optional): The number of retry attempts. Defaults to 3. + wait (float, optional): The waiting period between retries in seconds. Defaults to 10.0. + exceptions (List[Exception] or None, optional): List of exception types to catch and retry. + Defaults to None, which catches all exceptions. + + Returns: + 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]) + def my_function(): + # Function logic here + + Note: + The decorator catches specified exceptions and retries the function, logging + information about each retry attempt. + + """ + + exceptions = [Exception] if exceptions is None else exceptions + + def actual_decorator(func: Callable) -> Callable: + + @functools.wraps(func) + def decorated_func(*args, **kwargs): + previous_exception = None + for n in range(times): + try: + return func(*args, **kwargs) + except tuple(exceptions) as exc: + previous_exception = exc + if n < times: + MODULE_LOGGER.info( + f"Retry {n + 1}: {func.__name__} will be executing again in {wait}s. " + f"Received a {previous_exception!r}." + ) + time.sleep(wait) + raise previous_exception + return decorated_func + + return actual_decorator
  • @@ -760,6 +859,103 @@

    Returns

    return func +
    +def retry(times: int = 3, wait: float = 10.0, exceptions: List[~T] = None) +
    +
    +

    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.

    +

    Args

    +
    +
    times : int, optional
    +
    The number of retry attempts. Defaults to 3.
    +
    wait : float, optional
    +
    The waiting period between retries in seconds. Defaults to 10.0.
    +
    exceptions : List[Exception] or None, optional
    +
    List of exception types to catch and retry. +Defaults to None, which catches all exceptions.
    +
    +

    Returns

    +
    +
    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])
    +def my_function():
    +    # Function logic here
    +
    +

    Note

    +

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

    +
    + +Expand source code + +
    def retry(times: int = 3, wait: float = 10.0, exceptions: List = None):
    +    """
    +    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.
    +
    +    Args:
    +        times (int, optional): The number of retry attempts. Defaults to 3.
    +        wait (float, optional): The waiting period between retries in seconds. Defaults to 10.0.
    +        exceptions (List[Exception] or None, optional): List of exception types to catch and retry.
    +            Defaults to None, which catches all exceptions.
    +
    +    Returns:
    +        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])
    +            def my_function():
    +                # Function logic here
    +
    +    Note:
    +        The decorator catches specified exceptions and retries the function, logging
    +        information about each retry attempt.
    +
    +    """
    +
    +    exceptions = [Exception] if exceptions is None else exceptions
    +
    +    def actual_decorator(func: Callable) -> Callable:
    +
    +        @functools.wraps(func)
    +        def decorated_func(*args, **kwargs):
    +            previous_exception = None
    +            for n in range(times):
    +                try:
    +                    return func(*args, **kwargs)
    +                except tuple(exceptions) as exc:
    +                    previous_exception = exc
    +                if n < times:
    +                    MODULE_LOGGER.info(
    +                        f"Retry {n + 1}: {func.__name__} will be executing again in {wait}s. "
    +                        f"Received a {previous_exception!r}."
    +                    )
    +                    time.sleep(wait)
    +            raise previous_exception
    +        return decorated_func
    +
    +    return actual_decorator
    +
    +
    def singleton(cls)
    @@ -867,13 +1063,27 @@

    Examples

    def static_vars(**kwargs)
    -

    Define static variables in a function.

    +

    Define static variables in a function.

    +

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

    +
    @static_vars(count=0)
    +def special_count():
    +    return special_count.count += 2
    +
    Expand source code
    def static_vars(**kwargs):
    -    """Define static variables in a function."""
    +    """
    +    Define static variables in a function.
    +
    +    The static variable can be accessed with <function name>.<variable name> inside the function body.
    +
    +        @static_vars(count=0)
    +        def special_count():
    +            return special_count.count += 2
    +
    +    """
         def decorator(func):
             for kw in kwargs:
                 setattr(func, kw, kwargs[kw])
    @@ -1062,6 +1272,55 @@ 

    Args

    Classes

    +
    +class CountCalls +(func) +
    +
    +
    +
    + +Expand source code + +
    class CountCalls:
    +    def __init__(self, func):
    +        self.func = func
    +        self.count = 0
    +
    +    def __call__(self, *args, **kwargs):
    +        self.count += 1
    +        return self.func(*args, **kwargs)
    +
    +    def get_count(self):
    +        return self.count
    +
    +    # The __get__ method is here to make the decorator work with instance methods (methods inside a class)
    +    # as well. It ensures that when the decorated method is called on an instance, the self argument is
    +    # correctly passed to the method.
    +
    +    def __get__(self, instance, owner):
    +        if instance is None:
    +            return self
    +        else:
    +            return types.MethodType(self, instance)
    +
    +

    Methods

    +
    +
    +def get_count(self) +
    +
    +
    +
    + +Expand source code + +
    def get_count(self):
    +    return self.count
    +
    +
    +
    +
    class Nothing
    @@ -1077,6 +1336,39 @@

    Classes

    return "<Nothing>"
    +
    +class Profiler +
    +
    +
    +
    + +Expand source code + +
    class Profiler:
    +
    +    @classmethod
    +    def count(cls):
    +        return CountCalls
    +
    +

    Static methods

    +
    +
    +def count() +
    +
    +
    +
    + +Expand source code + +
    @classmethod
    +def count(cls):
    +    return CountCalls
    +
    +
    +
    +
    class classproperty (func) @@ -1151,6 +1443,7 @@

    Index

  • profile_func
  • query_command
  • read_command
  • +
  • retry
  • singleton
  • spy_on_attr_change
  • static_vars
  • @@ -1164,9 +1457,21 @@

    Index

  • Classes

    diff --git a/docs/api/egse/device.html b/docs/api/egse/device.html index 04a9193..92942e1 100644 --- a/docs/api/egse/device.html +++ b/docs/api/egse/device.html @@ -424,6 +424,7 @@

    Subclasses

  • OGSEEthernetInterface
  • DeviceInterface
  • AlphaPlusTelnetInterface
  • +
  • cdaq9375SocketInterface
  • cdaq9184SocketInterface
  • SerialDevice
  • SocketDevice
  • @@ -887,13 +888,14 @@

    Subclasses

  • BeagleboneInterface
  • OGSEInterface
  • FilterWheel8SMC4Interface
  • -
  • Fw8Smc5Interface
  • +
  • egse.filterwheel.eksma.fw8smc5_interface.Fw8Smc5Interface
  • GimbalInterface
  • AlphaControllerInterface
  • PunaInterface
  • ZondaInterface
  • BeagleboneInterface
  • LampEQ99Interface
  • +
  • cdaq9375Interface
  • cdaq9184Interface
  • ThorlabsPM100Interface
  • ShutterKSC101Interface
  • @@ -905,16 +907,15 @@

    Subclasses

  • TCSInterface
  • Agilent34970Interface
  • Agilent34972Interface
  • -
  • BeagleboneInterface
  • +
  • DigaloxInterface
  • DAQ6510Interface
  • -
  • LakeShore336Interface
  • -
  • PidInterface
  • +
  • LakeShoreInterface
  • ptc10Interface
  • APCInterface
  • BeagleboneInterface
  • Igm402Interface
  • Leo3Interface
  • -
  • VacscanInterface
  • +
  • EvisionInterface
  • Acp40Interface
  • Tc400Interface
  • Tpg261Interface
  • @@ -1108,6 +1109,7 @@

    Subclasses

  • DummyDeviceInterface
  • AlphaPlusTelnetInterface
  • ZondaTelnetInterface
  • +
  • cdaq9375SocketInterface
  • cdaq9184SocketInterface
  • SocketInterface
  • HuberSMC9300EthernetInterface
  • diff --git a/docs/api/egse/dpu/ccd_ui.html b/docs/api/egse/dpu/ccd_ui.html index 7815be0..527982e 100644 --- a/docs/api/egse/dpu/ccd_ui.html +++ b/docs/api/egse/dpu/ccd_ui.html @@ -51,20 +51,21 @@

    Module egse.dpu.ccd_ui

    from egse.gui.buttons import ToolTouchButton from egse.resource import get_resource from egse.spw import DataPacket -from egse.state import GlobalState LOGGER = logging.getLogger(__name__) class ImageCreator1: - def __init__(self, nr_lines: int, nr_columns: int): + # The original design where the image is build up by concatenating rows as they come in through data packets + + def __init__(self, nr_lines: int, nr_columns: int, n_fee_side): self.nr_lines = nr_lines self.nr_columns = nr_columns self.image_E = np.empty((0,), dtype=np.uint16) self.image_F = np.empty((0,), dtype=np.uint16) - self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum + self.n_fee_side = n_fee_side def add_data(self, data_packet: DataPacket): if data_packet.type.ccd_side == self.n_fee_side.E_SIDE: @@ -80,15 +81,17 @@

    Module egse.dpu.ccd_ui

    class ImageCreator: - def __init__(self, nr_lines: int, nr_columns: int): + + # This version allocates a partial CCD image and fills the data when it comes in through data packets + + def __init__(self, nr_lines: int, nr_columns: int, n_fee_side): self.nr_lines = nr_lines self.nr_columns = nr_columns self.index_E = self.index_F = 0 self.image_E = np.empty((nr_lines * nr_columns,), dtype=np.uint16) self.image_F = np.empty((nr_lines * nr_columns,), dtype=np.uint16) - self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum - + self.n_fee_side = n_fee_side def add_data(self, data_packet: DataPacket): data = data_packet.data_as_ndarray @@ -108,13 +111,16 @@

    Module egse.dpu.ccd_ui

    class ImageCreatorFullSize: + + # This version creates the full CCD images and fills the data when it comes in through data packets + MAX_NR_LINES = 4540 MAX_NR_COLUMNS = 2295 - def __init__(self, v_start: int, v_end: int, h_end: int): + def __init__(self, v_start: int, v_end: int, h_end: int, n_fee_side): # LOGGER.debug(f"{v_start=}, {v_end=}, {h_end=}, {id(self)=}") - self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum + self.n_fee_side = n_fee_side self.nr_lines = v_end - v_start + 1 self.nr_columns = h_end + 1 @@ -245,7 +251,7 @@

    Module egse.dpu.ccd_ui

    class NFEECCDWidget(QWidget): sig_maximize = pyqtSignal(int) - def __init__(self, ccd_number: Optional[int], maximize: bool = True, *args, **kwargs): + def __init__(self, ccd_number: Optional[int], n_fee_side, maximize: bool = True, *args, **kwargs): """ Args: @@ -256,7 +262,7 @@

    Module egse.dpu.ccd_ui

    """ super().__init__(*args, **kwargs) - self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum + self.n_fee_side = n_fee_side self._ccd_number = ccd_number # CGSE CCD number [1-4] self.ccd_label = QLabel(f"CCD #{self._ccd_number}") @@ -382,15 +388,17 @@

    Module egse.dpu.ccd_ui

    class NFEE4CCDWidget(QWidget): - def __init__(self): + def __init__(self, n_fee_side): super().__init__() + self.n_fee_side = n_fee_side + # CCD numbering here is CGSE CCD number [1-4] - self.ccd_1 = NFEECCDWidget(1, parent=self) - self.ccd_2 = NFEECCDWidget(2, parent=self) - self.ccd_3 = NFEECCDWidget(3, parent=self) - self.ccd_4 = NFEECCDWidget(4, parent=self) + self.ccd_1 = NFEECCDWidget(1, n_fee_side=self.n_fee_side, parent=self) + self.ccd_2 = NFEECCDWidget(2, n_fee_side=self.n_fee_side, parent=self) + self.ccd_3 = NFEECCDWidget(3, n_fee_side=self.n_fee_side, parent=self) + self.ccd_4 = NFEECCDWidget(4, n_fee_side=self.n_fee_side, parent=self) self.ccd_1.sig_maximize.connect(self.maximize_view) self.ccd_2.sig_maximize.connect(self.maximize_view) @@ -399,8 +407,6 @@

    Module egse.dpu.ccd_ui

    self.ccds = [self.ccd_1, self.ccd_2, self.ccd_3, self.ccd_4] - self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum - grid_layout = QGridLayout() grid_layout.addWidget(self.ccd_1, 0, 0) @@ -438,7 +444,7 @@

    Module egse.dpu.ccd_ui

    Args: ccd_number: CGSE CCD number [1-4] """ - widget = NFEECCDWidget(ccd_number, maximize=False) + widget = NFEECCDWidget(ccd_number, n_fee_side=self.n_fee_side, maximize=False) layout = QHBoxLayout() layout.addWidget(widget) @@ -583,37 +589,34 @@

    Classes

    for each frame, then playback is in realtime.

    By default, this class creates an :class:ImageItem <pyqtgraph.ImageItem> to display image data -and a :class:ViewBox <pyqtgraph.ViewBox> to contain the ImageItem.

    -

    ============= ========================================================= -Arguments -parent -(QWidget) Specifies the parent widget to which -this ImageView will belong. If None, then the ImageView -is created with no parent. -name -(str) The name used to register both the internal ViewBox -and the PlotItem used to display ROI data. See the name -argument to :func:ViewBox.__init__() -<pyqtgraph.ViewBox.__init__>. -view -(ViewBox or PlotItem) If specified, this will be used -as the display area that contains the displayed image. -Any :class:ViewBox <pyqtgraph.ViewBox>, -:class:PlotItem <pyqtgraph.PlotItem>, or other -compatible object is acceptable. -imageItem -(ImageItem) If specified, this object will be used to -display the image. Must be an instance of ImageItem -or other compatible object. -levelMode -See the levelMode argument to -:func:HistogramLUTItem.__init__() -<pyqtgraph.HistogramLUTItem.__init__> -============= =========================================================

    -

    Note: to display axis ticks inside the ImageView, instantiate it -with a PlotItem instance as its view::

    -
    pg.ImageView(view=pg.PlotItem())
    -
    +and a :class:ViewBox <pyqtgraph.ViewBox> to contain the ImageItem.

    +

    Parameters

    +
    +
    parent : QWidget
    +
    Specifies the parent widget to which this ImageView will belong. If None, then the ImageView is created with +no parent.
    +
    name : str
    +
    The name used to register both the internal ViewBox and the PlotItem used to display ROI data. See the +name argument to :func:ViewBox.__init__() <pyqtgraph.ViewBox.__init__>.
    +
    view : ViewBox or PlotItem
    +
    If specified, this will be used as the display area that contains the displayed image. Any +:class:ViewBox <pyqtgraph.ViewBox>, :class:PlotItem <pyqtgraph.PlotItem>, or other compatible object is +acceptable. Note: to display axis ticks inside the ImageView, instantiate it with a PlotItem instance as its +view::
    pg.ImageView(view=pg.PlotItem())
    +
    +
    +
    imageItem : ImageItem
    +
    If specified, this object will be used to display the image. Must be an instance of ImageItem or other +compatible object.
    +
    levelMode : str
    +
    See the levelMode argument to :func:HistogramLUTItem.__init__() <pyqtgraph.HistogramLUTItem.__init__>
    +
    discreteTimeLine : bool
    +
    Whether to snap to xvals / frame numbers when interacting with the timeline position.
    +
    roi : ROI
    +
    If specified, this object is used as ROI for the plot feature. Must be an instance of ROI.
    +
    normRoi : ROI
    +
    If specified, this object is used as ROI for the normalization feature. Must be an instance of ROI.
    +
    Expand source code @@ -695,7 +698,7 @@

    Methods

    (*args, axisItems=None, **kwargs)
    -

    QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -848,7 +851,7 @@

    Methods

    class ImageCreator -(nr_lines: int, nr_columns: int) +(nr_lines: int, nr_columns: int, n_fee_side)
    @@ -857,15 +860,17 @@

    Methods

    Expand source code
    class ImageCreator:
    -    def __init__(self, nr_lines: int, nr_columns: int):
    +
    +    # This version allocates a partial CCD image and fills the data when it comes in through data packets
    +
    +    def __init__(self, nr_lines: int, nr_columns: int, n_fee_side):
             self.nr_lines = nr_lines
             self.nr_columns = nr_columns
             self.index_E = self.index_F = 0
             self.image_E = np.empty((nr_lines * nr_columns,), dtype=np.uint16)
             self.image_F = np.empty((nr_lines * nr_columns,), dtype=np.uint16)
     
    -        self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum
    -
    +        self.n_fee_side = n_fee_side
     
         def add_data(self, data_packet: DataPacket):
             data = data_packet.data_as_ndarray
    @@ -925,7 +930,7 @@ 

    Methods

    class ImageCreator1 -(nr_lines: int, nr_columns: int) +(nr_lines: int, nr_columns: int, n_fee_side)
    @@ -935,13 +940,15 @@

    Methods

    class ImageCreator1:
     
    -    def __init__(self, nr_lines: int, nr_columns: int):
    +    # The original design where the image is build up by concatenating rows as they come in through data packets
    +
    +    def __init__(self, nr_lines: int, nr_columns: int, n_fee_side):
             self.nr_lines = nr_lines
             self.nr_columns = nr_columns
             self.image_E = np.empty((0,), dtype=np.uint16)
             self.image_F = np.empty((0,), dtype=np.uint16)
     
    -        self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum
    +        self.n_fee_side = n_fee_side
     
         def add_data(self, data_packet: DataPacket):
             if data_packet.type.ccd_side == self.n_fee_side.E_SIDE:
    @@ -993,7 +1000,7 @@ 

    Methods

    class ImageCreatorFullSize -(v_start: int, v_end: int, h_end: int) +(v_start: int, v_end: int, h_end: int, n_fee_side)
    @@ -1002,13 +1009,16 @@

    Methods

    Expand source code
    class ImageCreatorFullSize:
    +
    +    # This version creates the full CCD images and fills the data when it comes in through data packets
    +
         MAX_NR_LINES = 4540
         MAX_NR_COLUMNS = 2295
     
    -    def __init__(self, v_start: int, v_end: int, h_end: int):
    +    def __init__(self, v_start: int, v_end: int, h_end: int, n_fee_side):
             # LOGGER.debug(f"{v_start=}, {v_end=}, {h_end=}, {id(self)=}")
     
    -        self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum
    +        self.n_fee_side = n_fee_side
     
             self.nr_lines = v_end - v_start + 1
             self.nr_columns = h_end + 1
    @@ -1087,23 +1097,26 @@ 

    Methods

    class NFEE4CCDWidget +(n_fee_side)
    -

    QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code
    class NFEE4CCDWidget(QWidget):
    -    def __init__(self):
    +    def __init__(self, n_fee_side):
             super().__init__()
     
    +        self.n_fee_side = n_fee_side
    +
             # CCD numbering here is CGSE CCD number [1-4]
     
    -        self.ccd_1 = NFEECCDWidget(1, parent=self)
    -        self.ccd_2 = NFEECCDWidget(2, parent=self)
    -        self.ccd_3 = NFEECCDWidget(3, parent=self)
    -        self.ccd_4 = NFEECCDWidget(4, parent=self)
    +        self.ccd_1 = NFEECCDWidget(1, n_fee_side=self.n_fee_side, parent=self)
    +        self.ccd_2 = NFEECCDWidget(2, n_fee_side=self.n_fee_side, parent=self)
    +        self.ccd_3 = NFEECCDWidget(3, n_fee_side=self.n_fee_side, parent=self)
    +        self.ccd_4 = NFEECCDWidget(4, n_fee_side=self.n_fee_side, parent=self)
     
             self.ccd_1.sig_maximize.connect(self.maximize_view)
             self.ccd_2.sig_maximize.connect(self.maximize_view)
    @@ -1112,8 +1125,6 @@ 

    Methods

    self.ccds = [self.ccd_1, self.ccd_2, self.ccd_3, self.ccd_4] - self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum - grid_layout = QGridLayout() grid_layout.addWidget(self.ccd_1, 0, 0) @@ -1151,7 +1162,7 @@

    Methods

    Args: ccd_number: CGSE CCD number [1-4] """ - widget = NFEECCDWidget(ccd_number, maximize=False) + widget = NFEECCDWidget(ccd_number, n_fee_side=self.n_fee_side, maximize=False) layout = QHBoxLayout() layout.addWidget(widget) @@ -1258,7 +1269,7 @@

    Args

    Args: ccd_number: CGSE CCD number [1-4] """ - widget = NFEECCDWidget(ccd_number, maximize=False) + widget = NFEECCDWidget(ccd_number, n_fee_side=self.n_fee_side, maximize=False) layout = QHBoxLayout() layout.addWidget(widget) @@ -1328,10 +1339,10 @@

    Args

    class NFEECCDWidget -(ccd_number: Optional[int], maximize: bool = True, *args, **kwargs) +(ccd_number: Optional[int], n_fee_side, maximize: bool = True, *args, **kwargs)
    -

    QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Args

    ccd_number
    @@ -1348,7 +1359,7 @@

    Args

    class NFEECCDWidget(QWidget):
         sig_maximize = pyqtSignal(int)
     
    -    def __init__(self, ccd_number: Optional[int], maximize: bool = True, *args, **kwargs):
    +    def __init__(self, ccd_number: Optional[int], n_fee_side, maximize: bool = True, *args, **kwargs):
             """
     
             Args:
    @@ -1359,7 +1370,7 @@ 

    Args

    """ super().__init__(*args, **kwargs) - self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum + self.n_fee_side = n_fee_side self._ccd_number = ccd_number # CGSE CCD number [1-4] self.ccd_label = QLabel(f"CCD #{self._ccd_number}") @@ -1613,7 +1624,7 @@

    Methods

    (data)
    -

    QAbstractTableModel(parent: QObject = None)

    +

    QAbstractTableModel(parent: typing.Optional[QObject] = None)

    Expand source code @@ -1683,7 +1694,7 @@

    Methods

    def data(self, index, role)
    -

    data(self, QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    +

    data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    Expand source code @@ -1742,7 +1753,7 @@

    Methods

    class NFEEStateWidget
    -

    QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -1798,9 +1809,11 @@

    Ancestors

    showValues (bool) Whether to display values adjacent to ticks pen -(QPen) Pen used when drawing ticks. +(QPen) Pen used when drawing axis and (by default) ticks textPen (QPen) Pen used when drawing tick labels. +tickPen +(QPen) Pen used when drawing ticks. text The text (excluding units) to display on the label for this axis. diff --git a/docs/api/egse/dpu/dpu.html b/docs/api/egse/dpu/dpu.html index fb23e2d..14a7465 100644 --- a/docs/api/egse/dpu/dpu.html +++ b/docs/api/egse/dpu/dpu.html @@ -57,23 +57,21 @@

    Module egse.dpu.dpu

    from collections import namedtuple from typing import Optional -import numpy as np - from egse.fee import n_fee_mode from egse.reg import Register from egse.reg import RegisterMap from egse.spw import SpaceWireInterface -from egse.state import GlobalState from egse.system import Timer MODULE_LOGGER = logging.getLogger(__name__) DEFAULT_CCD_READOUT_ORDER = 0b11100100 -SENSOR_SEL_BOTH_SIDES = 3 """The default for pFM -> CCD 1, 2, 3, 4 (reading from the right). We use this default value only for going to DUMP mode because there the ccd_readout_order parameter is not passed. For all other command functions, the ccd_readout_order is explicitly passed.""" +SENSOR_SEL_BOTH_SIDES = 3 + class NFEEState: """ @@ -299,7 +297,7 @@

    Module egse.dpu.dpu

    register_map (RegisterMap): the N-FEE register map """ - # In principle we should request each register separately, but the N-FEE allows even in + # In principle, we should request each register separately, but the N-FEE allows even in # critical memory to read the full register at once. I leave the original code here should # the behaviour of the N-FEE become more restrictive again. # @@ -313,18 +311,36 @@

    Module egse.dpu.dpu

    return register_map -def command_set_nfee_fpga_defaults(transport: SpaceWireInterface, register_map: RegisterMap): - """ Set the default FPGA parameters for the N-FEE. +def command_get_hk_information(transport: SpaceWireInterface, register_map: RegisterMap, address: int, data_length: int) -> bytes: + """ + Reads the memory area of the N-FEE where the housekeeping information is saved. This area is located between + 0x0700 – 0x07FC (inclusive) [see PLATO-DLR-PL-ICD-010]. + + Args: + transport (SpaceWireInterface): interface to use for SpaceWire communication + register_map (RegisterMap): the N-FEE register map -> not used in this function + address (int): start address + data_length (int): number of bytes + + Returns: + A bytes object containing the housekeeping information. + """ + + return transport.read_memory_map(address, data_length) + + +def command_set_nfee_fpga_defaults(transport: SpaceWireInterface, register_map: RegisterMap, default_values: dict): + """ + Set the camera specific default FPGA parameters for the N-FEE. Args: + default_values (dict): FPGA defaults transport (SpaceWireInterface): interface to use for SpaceWire communication register_map (RegisterMap): the N-FEE register map """ MODULE_LOGGER.info("Set default values for the FPGA") - default_values = GlobalState.setup.camera.fee.fpga_defaults - for reg_name in default_values: hex_string = str(default_values[reg_name]) byte_string = int(hex_string, 16).to_bytes(length=4, byteorder='big', signed=False) @@ -474,9 +490,9 @@

    Module egse.dpu.dpu

    def command_set_dump_mode( transport: SpaceWireInterface, register_map: RegisterMap, - v_start: int = 0, v_end: int = 4509, sensor_sel_=SENSOR_SEL_BOTH_SIDES, + v_start: int = 0, v_end: int = 0, sensor_sel_=SENSOR_SEL_BOTH_SIDES, ccd_readout_order: int = DEFAULT_CCD_READOUT_ORDER, - n_final_dump: int = 0, sync_sel: int = 0 + n_final_dump: int = 4510, sync_sel: int = 0 ): MODULE_LOGGER.info("Commanding N-FEE into dump mode.") @@ -503,9 +519,9 @@

    Module egse.dpu.dpu

    def command_set_dump_mode_int_sync( transport: SpaceWireInterface, register_map: RegisterMap, - v_start: int = 0, v_end: int = 10, sensor_sel_=SENSOR_SEL_BOTH_SIDES, + v_start: int = 0, v_end: int = 0, sensor_sel_=SENSOR_SEL_BOTH_SIDES, ccd_readout_order: int = DEFAULT_CCD_READOUT_ORDER, - n_final_dump: int = 4509, int_sync_period: int = 2500, sync_sel: int = 1 + n_final_dump: int = 4510, int_sync_period: int = 600, sync_sel: int = 1 ): MODULE_LOGGER.info("Commanding N-FEE into internal sync dump mode.") @@ -527,7 +543,7 @@

    Module egse.dpu.dpu

    def _set_register(transport: SpaceWireInterface, register_map: RegisterMap, reg_name: str, **kwarg: int): """ - Set a register from it's individual parameters and sends the register to the N-FEE. This + Set a register from its individual parameters and sends the register to the N-FEE. This function first reads the register from the N-FEE and updates the local register if there is a mismatch. @@ -634,7 +650,7 @@

    Module egse.dpu.dpu

    transport: SpaceWireInterface, register_map: RegisterMap, ): - MODULE_LOGGER.debug("Commanding N-FEE to use the external clock.") + MODULE_LOGGER.info("Commanding N-FEE to use the external clock.") _set_register(transport, register_map, "reg_5_config", sync_sel=0) @@ -687,6 +703,7 @@

    Module egse.dpu.dpu

    the error conditions persist and no corrective measures are taken, then error flags would be set again. """ + # keep debug level because this command is sent on each readout MODULE_LOGGER.debug("Commanding N-FEE to clear error flag.") _set_register(transport, register_map, "reg_21_config", clear_error_flag=1) @@ -694,6 +711,15 @@

    Module egse.dpu.dpu

    return register_map["clear_error_flag"] +def command_set_readout_order(transport: SpaceWireInterface, register_map: RegisterMap, ccd_readout_order: int): + """ + Sets the given ccd_readout_order in the register map, then sends this change to the N-FEE. + """ + MODULE_LOGGER.info(f"Commanding N-FEE – set readout order to 0x{ccd_readout_order:02x}.") + + _set_register(transport, register_map, "reg_2_config", ccd_readout_order=ccd_readout_order) + + def command_set_reverse_clocking( transport: SpaceWireInterface, register_map: RegisterMap, @@ -801,7 +827,7 @@

    Module egse.dpu.dpu

    Global variables

    -
    var SENSOR_SEL_BOTH_SIDES
    +
    var DEFAULT_CCD_READOUT_ORDER

    The default for pFM -> CCD 1, 2, 3, 4 (reading from the right). We use this default value only for going to DUMP mode because there the ccd_readout_order parameter @@ -825,11 +851,52 @@

    Functions

    transport: SpaceWireInterface, register_map: RegisterMap, ): - MODULE_LOGGER.debug("Commanding N-FEE to use the external clock.") + MODULE_LOGGER.info("Commanding N-FEE to use the external clock.") _set_register(transport, register_map, "reg_5_config", sync_sel=0)
    +
    +def command_get_hk_information(transport: SpaceWireInterface, register_map: RegisterMap, address: int, data_length: int) ‑> bytes +
    +
    +

    Reads the memory area of the N-FEE where the housekeeping information is saved. This area is located between +0x0700 – 0x07FC (inclusive) [see PLATO-DLR-PL-ICD-010].

    +

    Args

    +
    +
    transport : SpaceWireInterface
    +
    interface to use for SpaceWire communication
    +
    register_map : RegisterMap
    +
    the N-FEE register map -> not used in this function
    +
    address : int
    +
    start address
    +
    data_length : int
    +
    number of bytes
    +
    +

    Returns

    +

    A bytes object containing the housekeeping information.

    +
    + +Expand source code + +
    def command_get_hk_information(transport: SpaceWireInterface, register_map: RegisterMap, address: int, data_length: int) -> bytes:
    +    """
    +    Reads the memory area of the N-FEE where the housekeeping information is saved. This area is located between
    +    0x0700 – 0x07FC (inclusive) [see PLATO-DLR-PL-ICD-010].
    +
    +    Args:
    +        transport (SpaceWireInterface): interface to use for SpaceWire communication
    +        register_map (RegisterMap): the N-FEE register map -> not used in this function
    +        address (int): start address
    +        data_length (int): number of bytes
    +
    +    Returns:
    +        A bytes object containing the housekeeping information.
    +    """
    +
    +    return transport.read_memory_map(address, data_length)
    +
    +
    def command_get_mode(transport: SpaceWireInterface, register_map: RegisterMap)
    @@ -974,6 +1041,7 @@

    Functions

    the error conditions persist and no corrective measures are taken, then error flags would be set again. """ + # keep debug level because this command is sent on each readout MODULE_LOGGER.debug("Commanding N-FEE to clear error flag.") _set_register(transport, register_map, "reg_21_config", clear_error_flag=1) @@ -982,7 +1050,7 @@

    Functions

    -def command_set_dump_mode(transport: SpaceWireInterface, register_map: RegisterMap, v_start: int = 0, v_end: int = 4509, sensor_sel_=3, ccd_readout_order: int = 228, n_final_dump: int = 0, sync_sel: int = 0) +def command_set_dump_mode(transport: SpaceWireInterface, register_map: RegisterMap, v_start: int = 0, v_end: int = 0, sensor_sel_=3, ccd_readout_order: int = 228, n_final_dump: int = 4510, sync_sel: int = 0)
    @@ -993,9 +1061,9 @@

    Functions

    def command_set_dump_mode(
             transport: SpaceWireInterface,
             register_map: RegisterMap,
    -        v_start: int = 0, v_end: int = 4509, sensor_sel_=SENSOR_SEL_BOTH_SIDES,
    +        v_start: int = 0, v_end: int = 0, sensor_sel_=SENSOR_SEL_BOTH_SIDES,
             ccd_readout_order: int = DEFAULT_CCD_READOUT_ORDER,
    -        n_final_dump: int = 0, sync_sel: int = 0
    +        n_final_dump: int = 4510, sync_sel: int = 0
     ):
         MODULE_LOGGER.info("Commanding N-FEE into dump mode.")
     
    @@ -1020,7 +1088,7 @@ 

    Functions

    -def command_set_dump_mode_int_sync(transport: SpaceWireInterface, register_map: RegisterMap, v_start: int = 0, v_end: int = 10, sensor_sel_=3, ccd_readout_order: int = 228, n_final_dump: int = 4509, int_sync_period: int = 2500, sync_sel: int = 1) +def command_set_dump_mode_int_sync(transport: SpaceWireInterface, register_map: RegisterMap, v_start: int = 0, v_end: int = 0, sensor_sel_=3, ccd_readout_order: int = 228, n_final_dump: int = 4510, int_sync_period: int = 600, sync_sel: int = 1)
    @@ -1031,9 +1099,9 @@

    Functions

    def command_set_dump_mode_int_sync(
             transport: SpaceWireInterface,
             register_map: RegisterMap,
    -        v_start: int = 0, v_end: int = 10, sensor_sel_=SENSOR_SEL_BOTH_SIDES,
    +        v_start: int = 0, v_end: int = 0, sensor_sel_=SENSOR_SEL_BOTH_SIDES,
             ccd_readout_order: int = DEFAULT_CCD_READOUT_ORDER,
    -        n_final_dump: int = 4509, int_sync_period: int = 2500, sync_sel: int = 1
    +        n_final_dump: int = 4510, int_sync_period: int = 600, sync_sel: int = 1
     ):
         MODULE_LOGGER.info("Commanding N-FEE into internal sync dump mode.")
     
    @@ -1238,12 +1306,14 @@ 

    Returns

    -def command_set_nfee_fpga_defaults(transport: SpaceWireInterface, register_map: RegisterMap) +def command_set_nfee_fpga_defaults(transport: SpaceWireInterface, register_map: RegisterMap, default_values: dict)
    -

    Set the default FPGA parameters for the N-FEE.

    +

    Set the camera specific default FPGA parameters for the N-FEE.

    Args

    +
    default_values : dict
    +
    FPGA defaults
    transport : SpaceWireInterface
    interface to use for SpaceWire communication
    register_map : RegisterMap
    @@ -1253,18 +1323,18 @@

    Args

    Expand source code -
    def command_set_nfee_fpga_defaults(transport: SpaceWireInterface, register_map: RegisterMap):
    -    """ Set the default FPGA parameters for the N-FEE.
    +
    def command_set_nfee_fpga_defaults(transport: SpaceWireInterface, register_map: RegisterMap, default_values: dict):
    +    """
    +    Set the camera specific default FPGA parameters for the N-FEE.
     
         Args:
    +        default_values (dict): FPGA defaults
             transport (SpaceWireInterface): interface to use for SpaceWire communication
             register_map (RegisterMap): the N-FEE register map
         """
     
         MODULE_LOGGER.info("Set default values for the FPGA")
     
    -    default_values = GlobalState.setup.camera.fee.fpga_defaults
    -
         for reg_name in default_values:
             hex_string = str(default_values[reg_name])
             byte_string = int(hex_string, 16).to_bytes(length=4, byteorder='big', signed=False)
    @@ -1294,6 +1364,24 @@ 

    Args

    return register_map["ccd_mode_config"]
    +
    +def command_set_readout_order(transport: SpaceWireInterface, register_map: RegisterMap, ccd_readout_order: int) +
    +
    +

    Sets the given ccd_readout_order in the register map, then sends this change to the N-FEE.

    +
    + +Expand source code + +
    def command_set_readout_order(transport: SpaceWireInterface, register_map: RegisterMap, ccd_readout_order: int):
    +    """
    +    Sets the given ccd_readout_order in the register map, then sends this change to the N-FEE.
    +    """
    +    MODULE_LOGGER.info(f"Commanding N-FEE – set readout order to 0x{ccd_readout_order:02x}.")
    +
    +    _set_register(transport, register_map, "reg_2_config", ccd_readout_order=ccd_readout_order)
    +
    +
    def command_set_register_value(transport: SpaceWireInterface, register_map: RegisterMap, reg_name: str, field_name: str, field_value: int)
    @@ -1441,7 +1529,7 @@

    Args

    register_map (RegisterMap): the N-FEE register map """ - # In principle we should request each register separately, but the N-FEE allows even in + # In principle, we should request each register separately, but the N-FEE allows even in # critical memory to read the full register at once. I leave the original code here should # the behaviour of the N-FEE become more restrictive again. # @@ -1496,7 +1584,7 @@

    Args

    -def prio_command_get_slicing(n_fee_state: , dpu_internals: DPUInternals, register_map: RegisterMap) ‑> int +def prio_command_get_slicing(n_fee_state: , dpu_internals: DPUInternals, register_map: RegisterMap) ‑> int
    @@ -1559,7 +1647,7 @@

    Args

    -def prio_command_set_slicing(n_fee_state: , dpu_internals: DPUInternals, register_map: RegisterMap, num_cycles: int) +def prio_command_set_slicing(n_fee_state: , dpu_internals: DPUInternals, register_map: RegisterMap, num_cycles: int)
    @@ -2169,12 +2257,13 @@

    Index

  • Global variables

  • Functions

    • command_external_clock
    • +
    • command_get_hk_information
    • command_get_mode
    • command_internal_clock
    • command_noop
    • @@ -2190,6 +2279,7 @@

      Index

    • command_set_immediate_on_mode
    • command_set_nfee_fpga_defaults
    • command_set_on_mode
    • +
    • command_set_readout_order
    • command_set_register_value
    • command_set_reverse_clocking
    • command_set_standby_mode
    • diff --git a/docs/api/egse/dpu/dpu_cs.html b/docs/api/egse/dpu/dpu_cs.html index 6e0fb84..ea9e4bd 100644 --- a/docs/api/egse/dpu/dpu_cs.html +++ b/docs/api/egse/dpu/dpu_cs.html @@ -73,8 +73,6 @@

      Module egse.dpu.dpu_cs

      import zmq from prometheus_client import start_http_server -multiprocessing.current_process().name = "dpu_cs" - from egse.control import ControlServer from egse.control import is_control_server_active from egse.dpu import DPUProtocol @@ -154,6 +152,8 @@

      Module egse.dpu.dpu_cs

      def start_dpu_simulator(transport: SpaceWireInterface): + multiprocessing.current_process().name = "dpu_cs" + try: dpu_sim = DPUControlServer(transport) dpu_sim.serve() @@ -366,6 +366,8 @@

      Returns

      def start_dpu_simulator(transport: SpaceWireInterface):
       
      +    multiprocessing.current_process().name = "dpu_cs"
      +
           try:
               dpu_sim = DPUControlServer(transport)
               dpu_sim.serve()
      @@ -526,10 +528,15 @@ 

      Inherited members

      • ControlServer:
      • diff --git a/docs/api/egse/dpu/dpu_ui.html b/docs/api/egse/dpu/dpu_ui.html index 5e4c996..9090f48 100644 --- a/docs/api/egse/dpu/dpu_ui.html +++ b/docs/api/egse/dpu/dpu_ui.html @@ -43,16 +43,24 @@

        Module egse.dpu.dpu_ui

        import multiprocessing import os import pickle +import sys import threading +from functools import partial from pathlib import Path -import sys -from PyQt5.QtCore import QLockFile, QModelIndex -from PyQt5.QtWidgets import QMessageBox, QTableView +from PyQt5.QtCore import QLockFile +from PyQt5.QtCore import QModelIndex +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont +from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtWidgets import QTableView +from egse.bits import bit_set from egse.dpu.hdf5_model import RegisterTableModel +from egse.gui.led import Indic +from egse.gui.led import LED from egse.setup import SetupError -from egse.state import GlobalState +from egse.setup import load_setup multiprocessing.current_process().name = "dpu_ui" @@ -86,7 +94,7 @@

        Module egse.dpu.dpu_ui

        from egse.system import do_every from egse.zmq import MessageIdentifier -LOGGER = logging.getLogger(__name__) +LOGGER = logging.getLogger("egse.dpu.dpu_ui") DPU_SETTINGS = Settings.load("DPU Processor") GUI_SETTINGS = Settings.load("DPU GUI") @@ -112,6 +120,51 @@

        Module egse.dpu.dpu_ui

        finished = pyqtSignal() error = pyqtSignal(str) data = pyqtSignal(int, object) + stalled = pyqtSignal(bool) + + +class DataMonitor(QRunnable): + def __init__(self, hostname: str, port: int): + super().__init__() + self.signals = WorkerSignals() + self.hostname = hostname + self.port = port + self.quit_request = False + self.receiver = None + + @pyqtSlot() + def run(self): + + context = zmq.Context.instance() + self.receiver = context.socket(zmq.SUB) + self.receiver.subscribe("") + + LOGGER.debug(f"Data Monitor connecting to {self.hostname}:{self.port}") + + self.receiver.connect(f"tcp://{self.hostname}:{self.port}") + + while True: + if self.quit_request: + break + + socket_list, _, _ = zmq.select([self.receiver], [], [], timeout=1.0) + + if self.receiver in socket_list: + try: + sync_id, pickle_string = self.receiver.recv_multipart() + sync_id = int.from_bytes(sync_id, byteorder='big') + data = pickle.loads(pickle_string) + self.signals.data.emit(sync_id, data) + except Exception as exc: + LOGGER.error("Exception caught!", exc_info=True) + self.signals.error.emit(str(exc)) + + self.receiver.disconnect(f"tcp://{self.hostname}:{self.port}") + self.receiver.close() + self.signals.finished.emit() + + def quit(self): + self.quit_request = True class DataPuller(QRunnable): @@ -126,6 +179,9 @@

        Module egse.dpu.dpu_ui

        @pyqtSlot() def run(self): + n_timeouts = 0 + """The timeout is 1s, we count the number of timeouts to detect if the DPU or N-FEE might be dead or stalled.""" + context = zmq.Context.instance() self.receiver = context.socket(zmq.SUB) self.receiver.setsockopt_string(zmq.SUBSCRIBE, "") @@ -137,6 +193,8 @@

        Module egse.dpu.dpu_ui

        # Needs further investigation.... perhaps we should go back to PUB–SUB... # We went back to PUB-SUB... + LOGGER.debug(f"Data Puller connecting to {self.hostname}:{self.port}") + self.receiver.connect(f"tcp://{self.hostname}:{self.port}") while True: @@ -146,6 +204,7 @@

        Module egse.dpu.dpu_ui

        socket_list, _, _ = zmq.select([self.receiver], [], [], timeout=1.0) if self.receiver in socket_list: + n_timeouts = 0 try: sync_id, pickle_string = self.receiver.recv_multipart() sync_id = int.from_bytes(sync_id, byteorder='big') @@ -155,6 +214,11 @@

        Module egse.dpu.dpu_ui

        LOGGER.error("Exception caught!", exc_info=True) self.signals.error.emit(str(exc)) + if len(socket_list) == 0: + n_timeouts += 1 + if n_timeouts > 7: # at least a timecode should arrive every 6.25s + self.signals.stalled.emit(True) + self.receiver.disconnect(f"tcp://{self.hostname}:{self.port}") self.receiver.close() self.signals.finished.emit() @@ -176,7 +240,7 @@

        Module egse.dpu.dpu_ui

        vbox.setSpacing(4) vbox.setContentsMargins(10, 0, 10, 0) - vbox.addWidget(QLabel("Mode Parameters")) + vbox.addWidget(QLabel("<b>Mode Parameters</b>")) for name, widget in self.params.items(): hbox = QHBoxLayout() @@ -207,7 +271,7 @@

        Module egse.dpu.dpu_ui

        vbox.setSpacing(4) vbox.setContentsMargins(10, 0, 10, 0) - vbox.addWidget(QLabel("Readout Parameters")) + vbox.addWidget(QLabel("<b>Readout Parameters</b>")) for name, widget in self.params.items(): hbox = QHBoxLayout() @@ -228,6 +292,109 @@

        Module egse.dpu.dpu_ui

        self.params[name].setText(f"{value}") +class StatusParameterWidget(QWidget): + def __init__(self, params: dict): + super().__init__() + + self.params = params + self.error_flag_names = [ + "Wrong X-Coordinate", "Wrong Y-Coordinate", "E SRAM Full", "F SRAM Full", "AWLA Error", + "SRAM EDAC Correct", "SRAM EDAC Uncorrect", "Block R EDAC", + "Disconnect Error", "Escape Error", "Credit Error", "Parity Error", "Lock Error", + ] + + # Use the following line to put a small border around all the parameters + # self.setStyleSheet('border: 1px solid red;') + + vbox = QVBoxLayout() + vbox.setSpacing(4) + vbox.setContentsMargins(10, 0, 10, 0) + + hbox = QHBoxLayout() + hbox.setAlignment(Qt.AlignLeft) + hbox.addWidget(QLabel("<b>Status Parameters</b> (")) + clear_error_flags = QLabel("Clear") + clear_error_flags.setStyleSheet("color: blue; text-decoration: underline") + clear_error_flags.mousePressEvent = self.clear_error_flags + hbox.addWidget(clear_error_flags) + hbox.addWidget(QLabel(")")) + vbox.addLayout(hbox) + + for name, item in self.params.items(): + self.set_format(name, 0) + self.update_param(name, 0) + hbox = QHBoxLayout() + hbox.addWidget(QLabel(name)) + hbox.addWidget(item['led']) + label: QLabel = item['label'] + font = label.font() + font.setStyleHint(QFont.TypeWriter) + label.setFont(font) + label.mousePressEvent = partial(self.toggle_format, name) + hbox.addWidget(label) + vbox.addLayout(hbox) + + vbox.addStretch() + self.setLayout(vbox) + + def toggle_format(self, name, event): + LOGGER.info(f"{name = }, {event = }") + format_type = self.params[name]['format_type'] + 1 + format_type = self.params[name]['format_type'] = format_type if 0 <= format_type < 3 else 0 + self.set_format(name, format_type) + self.refresh_param(name) + + def set_format(self, name, format_type): + LOGGER.info(f"{name = }, {format_type = }") + formats = ["0x{0:04X}", "0b{0:012b}", " {0:d}"] + self.params[name]['format_type'] = format_type + self.params[name]['format'] = formats[format_type] + + def set_value(self, name, value): + self.params[name]['value'] = value + + def get_value(self, name): + return self.params[name]['value'] + + def refresh_param(self, name): + self.update_param(name, self.params[name]['value']) + + def clear_error_flags(self, *args): + self.update_param("Error Flags", 0, "no errors detected") + + def update_param(self, name, value, tooltip=None): + format = self.params[name]['format'] + self.params[name]['value'] = value + self.params[name]['label'].setText(format.format(value)) + if tooltip: + self.params[name]['label'].setToolTip(tooltip) + self.params[name]['led'].set_color(Indic.BLACK if value == 0 else self.params[name]['color']) + + def update_params(self, params: dict): + LOGGER.info(f"{params = }") + for name, data in params.items(): + if name == "Error Flags": + error_flags, frame_counter, timestamp = data + if self.params[name]['value'] == 0 and error_flags != 0: + self.update_param(name, error_flags, self._decode_error_flags(error_flags, frame_counter, timestamp)) + # self.refresh_param(name) + + def _decode_error_flags(self, error_flags, frame_counter, timestamp) -> str: + + if error_flags == 0: + return "no errors detected" + + msg = f"<b>Error on {frame_counter=} </b><br><br>" + + flags = "".join([ + f"– {name}<br>" + for idx, name in enumerate(self.error_flag_names) + if bit_set(error_flags, idx) + ]) + + return msg + flags + + class MainWindow(QMainWindow): def __init__(self): super().__init__() @@ -240,7 +407,9 @@

        Module egse.dpu.dpu_ui

        self.image = None self.register_map = None self.data_puller = None - self.setup = GlobalState.setup + self.data_monitor = None + self.setup = load_setup() + self.n_fee_side = self.setup.camera.fee.ccd_sides.enum try: self.ccd_bin_to_id = self.setup.camera.fee.ccd_numbering.CCD_BIN_TO_ID @@ -257,7 +426,7 @@

        Module egse.dpu.dpu_ui

        except AttributeError: raise SetupError("No entry in the setup for camera.fee.sensor_sel") - self.threadpool = QThreadPool() + self.threadpool = QThreadPool.globalInstance() self.counter = 0 @@ -267,7 +436,7 @@

        Module egse.dpu.dpu_ui

        # self.tabs.setStyleSheet('border: 1px solid green;') self.hk_box = NFEEHousekeepingWidget() - self.ccd_box = NFEE4CCDWidget() + self.ccd_box = NFEE4CCDWidget(self.n_fee_side) self.register_box = RegisterWidget() mode_params = { @@ -291,6 +460,17 @@

        Module egse.dpu.dpu_ui

        self.readout_params = ReadoutParameterWidget(readout_params) + status_params = { + 'Error Flags': { + 'led': LED(), + 'color': Indic.RED, + 'label': QLabel('0b000000000000'), + 'retain': True, + }, + } + + self.status_params = StatusParameterWidget(status_params) + self.tabs.addTab(self.ccd_box, "CCD Display") self.tabs.addTab(self.hk_box, "HK Packet") self.tabs.addTab(self.register_box, "Register Map") @@ -299,6 +479,7 @@

        Module egse.dpu.dpu_ui

        params_box.addWidget(self.mode_params) params_box.addWidget(self.readout_params) + params_box.addWidget(self.status_params) vbox = QVBoxLayout() vbox.setSpacing(0) # limit the space between the views and the parameters @@ -312,10 +493,12 @@

        Module egse.dpu.dpu_ui

        self.show() self.start_pulling_data() + self.start_monitoring() def closeEvent(self, event): LOGGER.info("Window closed, quiting....") self.data_puller.quit() + self.data_monitor.quit() event.accept() all_threads_finished = self.threadpool.waitForDone(msecs=2000) @@ -325,12 +508,39 @@

        Module egse.dpu.dpu_ui

        def start_pulling_data(self): self.data_puller = worker = DataPuller(DPU_SETTINGS.HOSTNAME, DPU_SETTINGS.DATA_DISTRIBUTION_PORT) self.threadpool.start(worker) - worker.signals.data.connect(self.worker_output) - worker.signals.finished.connect(self.worker_complete) - worker.signals.error.connect(self.worker_error) + worker.signals.data.connect(self.puller_output) + worker.signals.finished.connect(self.puller_complete) + worker.signals.error.connect(self.puller_error) + worker.signals.stalled.connect(self.puller_stalled) + + def start_monitoring(self): + self.data_monitor = worker = DataMonitor(DPU_SETTINGS.HOSTNAME, DPU_SETTINGS.MONITORING_PORT) + self.threadpool.start(worker) + worker.signals.data.connect(self.monitor_output) + worker.signals.finished.connect(self.monitor_complete) + worker.signals.error.connect(self.monitor_error) + + @pyqtSlot(int, bytes) + def monitor_output(self, sync_id, data): + LOGGER.info(f"Data Monitor: sync_id = {MessageIdentifier(sync_id).name}, {data = }") + if sync_id == MessageIdentifier.SYNC_ERROR_FLAGS: + params = { + 'Error Flags': data + } + self.status_params.update_params(params) + + @pyqtSlot() + def monitor_complete(self): + LOGGER.info("Data Monitor thread complete!") + # zmq.Context().destroy(linger=0) + + @pyqtSlot(str) + def monitor_error(self, t): + LOGGER.warning(f"Data Monitor ERROR: {t}") @pyqtSlot(int, bytes) - def worker_output(self, sync_id, data): + def puller_output(self, sync_id, data): + # LOGGER.info(f"Data Puller: sync_id = {MessageIdentifier(sync_id).name}") # Generates a lot of output! if sync_id == MessageIdentifier.SYNC_TIMECODE: timecode, timestamp = data # LOGGER.info(f"TIMECODE: {timecode=}, {timestamp=}") @@ -351,6 +561,7 @@

        Module egse.dpu.dpu_ui

        v_start=self.register_map["v_start"], v_end=self.register_map["v_end"], h_end=self.register_map["h_end"], + n_fee_side=self.n_fee_side, ) self.register_box.update(self.register_map) if sync_id == MessageIdentifier.SYNC_DATA_PACKET: @@ -396,13 +607,24 @@

        Module egse.dpu.dpu_ui

        self.readout_params.update_par('Number of Cycles', max(0, data)) @pyqtSlot() - def worker_complete(self): - LOGGER.warning("THREAD COMPLETE!") - zmq.Context().destroy(linger=0) + def puller_complete(self): + LOGGER.info("Data Puller thread complete!") + # zmq.Context().destroy(linger=0) @pyqtSlot(str) - def worker_error(self, t): - LOGGER.warning("ERROR: %s" % t) + def puller_error(self, t): + LOGGER.warning(f"Data Puller ERROR: {t}") + + @pyqtSlot(bool) + def puller_stalled(self, flag: bool): + params = { + 'N-FEE Mode': 'CONNECTION LOST', + 'N-FEE Cycle Time': "", + 'DUMP Mode': "", + 'Internal Sync': "", + 'External Sync': "", + } + self.mode_params.update_params(params) def mark_ccd_widget(self, ccd_number: int): self.ccd_box.mark(self.ccd_bin_to_idx[ccd_number]) @@ -540,13 +762,131 @@

        Functions

        Classes

        +
        +class DataMonitor +(hostname: str, port: int) +
        +
        +

        QRunnable() +QRunnable(a0: QRunnable)

        +
        + +Expand source code + +
        class DataMonitor(QRunnable):
        +    def __init__(self, hostname: str, port: int):
        +        super().__init__()
        +        self.signals = WorkerSignals()
        +        self.hostname = hostname
        +        self.port = port
        +        self.quit_request = False
        +        self.receiver = None
        +
        +    @pyqtSlot()
        +    def run(self):
        +
        +        context = zmq.Context.instance()
        +        self.receiver = context.socket(zmq.SUB)
        +        self.receiver.subscribe("")
        +
        +        LOGGER.debug(f"Data Monitor connecting to {self.hostname}:{self.port}")
        +
        +        self.receiver.connect(f"tcp://{self.hostname}:{self.port}")
        +
        +        while True:
        +            if self.quit_request:
        +                break
        +
        +            socket_list, _, _ = zmq.select([self.receiver], [], [], timeout=1.0)
        +
        +            if self.receiver in socket_list:
        +                try:
        +                    sync_id, pickle_string = self.receiver.recv_multipart()
        +                    sync_id = int.from_bytes(sync_id, byteorder='big')
        +                    data = pickle.loads(pickle_string)
        +                    self.signals.data.emit(sync_id, data)
        +                except Exception as exc:
        +                    LOGGER.error("Exception caught!", exc_info=True)
        +                    self.signals.error.emit(str(exc))
        +
        +        self.receiver.disconnect(f"tcp://{self.hostname}:{self.port}")
        +        self.receiver.close()
        +        self.signals.finished.emit()
        +
        +    def quit(self):
        +        self.quit_request = True
        +
        +

        Ancestors

        +
          +
        • PyQt5.QtCore.QRunnable
        • +
        • sip.wrapper
        • +
        • sip.simplewrapper
        • +
        +

        Methods

        +
        +
        +def quit(self) +
        +
        +
        +
        + +Expand source code + +
        def quit(self):
        +    self.quit_request = True
        +
        +
        +
        +def run(self) +
        +
        +

        run(self)

        +
        + +Expand source code + +
        @pyqtSlot()
        +def run(self):
        +
        +    context = zmq.Context.instance()
        +    self.receiver = context.socket(zmq.SUB)
        +    self.receiver.subscribe("")
        +
        +    LOGGER.debug(f"Data Monitor connecting to {self.hostname}:{self.port}")
        +
        +    self.receiver.connect(f"tcp://{self.hostname}:{self.port}")
        +
        +    while True:
        +        if self.quit_request:
        +            break
        +
        +        socket_list, _, _ = zmq.select([self.receiver], [], [], timeout=1.0)
        +
        +        if self.receiver in socket_list:
        +            try:
        +                sync_id, pickle_string = self.receiver.recv_multipart()
        +                sync_id = int.from_bytes(sync_id, byteorder='big')
        +                data = pickle.loads(pickle_string)
        +                self.signals.data.emit(sync_id, data)
        +            except Exception as exc:
        +                LOGGER.error("Exception caught!", exc_info=True)
        +                self.signals.error.emit(str(exc))
        +
        +    self.receiver.disconnect(f"tcp://{self.hostname}:{self.port}")
        +    self.receiver.close()
        +    self.signals.finished.emit()
        +
        +
        +
        +
        class DataPuller (hostname: str, port: int)

        QRunnable() -QRunnable(QRunnable)

        +QRunnable(a0: QRunnable)

        Expand source code @@ -563,6 +903,9 @@

        Classes

        @pyqtSlot() def run(self): + n_timeouts = 0 + """The timeout is 1s, we count the number of timeouts to detect if the DPU or N-FEE might be dead or stalled.""" + context = zmq.Context.instance() self.receiver = context.socket(zmq.SUB) self.receiver.setsockopt_string(zmq.SUBSCRIBE, "") @@ -574,6 +917,8 @@

        Classes

        # Needs further investigation.... perhaps we should go back to PUB–SUB... # We went back to PUB-SUB... + LOGGER.debug(f"Data Puller connecting to {self.hostname}:{self.port}") + self.receiver.connect(f"tcp://{self.hostname}:{self.port}") while True: @@ -583,6 +928,7 @@

        Classes

        socket_list, _, _ = zmq.select([self.receiver], [], [], timeout=1.0) if self.receiver in socket_list: + n_timeouts = 0 try: sync_id, pickle_string = self.receiver.recv_multipart() sync_id = int.from_bytes(sync_id, byteorder='big') @@ -592,6 +938,11 @@

        Classes

        LOGGER.error("Exception caught!", exc_info=True) self.signals.error.emit(str(exc)) + if len(socket_list) == 0: + n_timeouts += 1 + if n_timeouts > 7: # at least a timecode should arrive every 6.25s + self.signals.stalled.emit(True) + self.receiver.disconnect(f"tcp://{self.hostname}:{self.port}") self.receiver.close() self.signals.finished.emit() @@ -632,6 +983,9 @@

        Methods

        @pyqtSlot()
         def run(self):
         
        +    n_timeouts = 0
        +    """The timeout is 1s, we count the number of timeouts to detect if the DPU or N-FEE might be dead or stalled."""
        +
             context = zmq.Context.instance()
             self.receiver = context.socket(zmq.SUB)
             self.receiver.setsockopt_string(zmq.SUBSCRIBE, "")
        @@ -643,6 +997,8 @@ 

        Methods

        # Needs further investigation.... perhaps we should go back to PUB–SUB... # We went back to PUB-SUB... + LOGGER.debug(f"Data Puller connecting to {self.hostname}:{self.port}") + self.receiver.connect(f"tcp://{self.hostname}:{self.port}") while True: @@ -652,6 +1008,7 @@

        Methods

        socket_list, _, _ = zmq.select([self.receiver], [], [], timeout=1.0) if self.receiver in socket_list: + n_timeouts = 0 try: sync_id, pickle_string = self.receiver.recv_multipart() sync_id = int.from_bytes(sync_id, byteorder='big') @@ -661,6 +1018,11 @@

        Methods

        LOGGER.error("Exception caught!", exc_info=True) self.signals.error.emit(str(exc)) + if len(socket_list) == 0: + n_timeouts += 1 + if n_timeouts > 7: # at least a timecode should arrive every 6.25s + self.signals.stalled.emit(True) + self.receiver.disconnect(f"tcp://{self.hostname}:{self.port}") self.receiver.close() self.signals.finished.emit()
        @@ -672,7 +1034,7 @@

        Methods

        class MainWindow
        -

        QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

        +

        QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

        Expand source code @@ -689,7 +1051,9 @@

        Methods

        self.image = None self.register_map = None self.data_puller = None - self.setup = GlobalState.setup + self.data_monitor = None + self.setup = load_setup() + self.n_fee_side = self.setup.camera.fee.ccd_sides.enum try: self.ccd_bin_to_id = self.setup.camera.fee.ccd_numbering.CCD_BIN_TO_ID @@ -706,7 +1070,7 @@

        Methods

        except AttributeError: raise SetupError("No entry in the setup for camera.fee.sensor_sel") - self.threadpool = QThreadPool() + self.threadpool = QThreadPool.globalInstance() self.counter = 0 @@ -716,7 +1080,7 @@

        Methods

        # self.tabs.setStyleSheet('border: 1px solid green;') self.hk_box = NFEEHousekeepingWidget() - self.ccd_box = NFEE4CCDWidget() + self.ccd_box = NFEE4CCDWidget(self.n_fee_side) self.register_box = RegisterWidget() mode_params = { @@ -740,6 +1104,17 @@

        Methods

        self.readout_params = ReadoutParameterWidget(readout_params) + status_params = { + 'Error Flags': { + 'led': LED(), + 'color': Indic.RED, + 'label': QLabel('0b000000000000'), + 'retain': True, + }, + } + + self.status_params = StatusParameterWidget(status_params) + self.tabs.addTab(self.ccd_box, "CCD Display") self.tabs.addTab(self.hk_box, "HK Packet") self.tabs.addTab(self.register_box, "Register Map") @@ -748,6 +1123,7 @@

        Methods

        params_box.addWidget(self.mode_params) params_box.addWidget(self.readout_params) + params_box.addWidget(self.status_params) vbox = QVBoxLayout() vbox.setSpacing(0) # limit the space between the views and the parameters @@ -761,10 +1137,12 @@

        Methods

        self.show() self.start_pulling_data() + self.start_monitoring() def closeEvent(self, event): LOGGER.info("Window closed, quiting....") self.data_puller.quit() + self.data_monitor.quit() event.accept() all_threads_finished = self.threadpool.waitForDone(msecs=2000) @@ -774,12 +1152,39 @@

        Methods

        def start_pulling_data(self): self.data_puller = worker = DataPuller(DPU_SETTINGS.HOSTNAME, DPU_SETTINGS.DATA_DISTRIBUTION_PORT) self.threadpool.start(worker) - worker.signals.data.connect(self.worker_output) - worker.signals.finished.connect(self.worker_complete) - worker.signals.error.connect(self.worker_error) + worker.signals.data.connect(self.puller_output) + worker.signals.finished.connect(self.puller_complete) + worker.signals.error.connect(self.puller_error) + worker.signals.stalled.connect(self.puller_stalled) + + def start_monitoring(self): + self.data_monitor = worker = DataMonitor(DPU_SETTINGS.HOSTNAME, DPU_SETTINGS.MONITORING_PORT) + self.threadpool.start(worker) + worker.signals.data.connect(self.monitor_output) + worker.signals.finished.connect(self.monitor_complete) + worker.signals.error.connect(self.monitor_error) + + @pyqtSlot(int, bytes) + def monitor_output(self, sync_id, data): + LOGGER.info(f"Data Monitor: sync_id = {MessageIdentifier(sync_id).name}, {data = }") + if sync_id == MessageIdentifier.SYNC_ERROR_FLAGS: + params = { + 'Error Flags': data + } + self.status_params.update_params(params) + + @pyqtSlot() + def monitor_complete(self): + LOGGER.info("Data Monitor thread complete!") + # zmq.Context().destroy(linger=0) + + @pyqtSlot(str) + def monitor_error(self, t): + LOGGER.warning(f"Data Monitor ERROR: {t}") @pyqtSlot(int, bytes) - def worker_output(self, sync_id, data): + def puller_output(self, sync_id, data): + # LOGGER.info(f"Data Puller: sync_id = {MessageIdentifier(sync_id).name}") # Generates a lot of output! if sync_id == MessageIdentifier.SYNC_TIMECODE: timecode, timestamp = data # LOGGER.info(f"TIMECODE: {timecode=}, {timestamp=}") @@ -800,6 +1205,7 @@

        Methods

        v_start=self.register_map["v_start"], v_end=self.register_map["v_end"], h_end=self.register_map["h_end"], + n_fee_side=self.n_fee_side, ) self.register_box.update(self.register_map) if sync_id == MessageIdentifier.SYNC_DATA_PACKET: @@ -845,13 +1251,24 @@

        Methods

        self.readout_params.update_par('Number of Cycles', max(0, data)) @pyqtSlot() - def worker_complete(self): - LOGGER.warning("THREAD COMPLETE!") - zmq.Context().destroy(linger=0) + def puller_complete(self): + LOGGER.info("Data Puller thread complete!") + # zmq.Context().destroy(linger=0) @pyqtSlot(str) - def worker_error(self, t): - LOGGER.warning("ERROR: %s" % t) + def puller_error(self, t): + LOGGER.warning(f"Data Puller ERROR: {t}") + + @pyqtSlot(bool) + def puller_stalled(self, flag: bool): + params = { + 'N-FEE Mode': 'CONNECTION LOST', + 'N-FEE Cycle Time': "", + 'DUMP Mode': "", + 'Internal Sync': "", + 'External Sync': "", + } + self.mode_params.update_params(params) def mark_ccd_widget(self, ccd_number: int): self.ccd_box.mark(self.ccd_bin_to_idx[ccd_number])
      @@ -871,7 +1288,7 @@

      Methods

      def closeEvent(self, event)
      -

      closeEvent(self, QCloseEvent)

      +

      closeEvent(self, a0: typing.Optional[QCloseEvent])

      Expand source code @@ -879,6 +1296,7 @@

      Methods

      def closeEvent(self, event):
           LOGGER.info("Window closed, quiting....")
           self.data_puller.quit()
      +    self.data_monitor.quit()
           event.accept()
       
           all_threads_finished = self.threadpool.waitForDone(msecs=2000)
      @@ -899,8 +1317,8 @@ 

      Methods

      self.ccd_box.mark(self.ccd_bin_to_idx[ccd_number])
      -
      -def start_pulling_data(self) +
      +def monitor_complete(self)
      @@ -908,16 +1326,47 @@

      Methods

      Expand source code -
      def start_pulling_data(self):
      -    self.data_puller = worker = DataPuller(DPU_SETTINGS.HOSTNAME, DPU_SETTINGS.DATA_DISTRIBUTION_PORT)
      -    self.threadpool.start(worker)
      -    worker.signals.data.connect(self.worker_output)
      -    worker.signals.finished.connect(self.worker_complete)
      -    worker.signals.error.connect(self.worker_error)
      +
      @pyqtSlot()
      +def monitor_complete(self):
      +    LOGGER.info("Data Monitor thread complete!")
      +    # zmq.Context().destroy(linger=0)
      + +
      +
      +def monitor_error(self, t) +
      +
      +
      +
      + +Expand source code + +
      @pyqtSlot(str)
      +def monitor_error(self, t):
      +    LOGGER.warning(f"Data Monitor ERROR: {t}")
      +
      +
      +
      +def monitor_output(self, sync_id, data) +
      +
      +
      +
      + +Expand source code + +
      @pyqtSlot(int, bytes)
      +def monitor_output(self, sync_id, data):
      +    LOGGER.info(f"Data Monitor: sync_id = {MessageIdentifier(sync_id).name}, {data = }")
      +    if sync_id == MessageIdentifier.SYNC_ERROR_FLAGS:
      +        params = {
      +            'Error Flags': data
      +        }
      +        self.status_params.update_params(params)
      -
      -def worker_complete(self) +
      +def puller_complete(self)
      @@ -926,13 +1375,13 @@

      Methods

      Expand source code
      @pyqtSlot()
      -def worker_complete(self):
      -    LOGGER.warning("THREAD COMPLETE!")
      -    zmq.Context().destroy(linger=0)
      +def puller_complete(self): + LOGGER.info("Data Puller thread complete!") + # zmq.Context().destroy(linger=0)
      -
      -def worker_error(self, t) +
      +def puller_error(self, t)
      @@ -941,12 +1390,12 @@

      Methods

      Expand source code
      @pyqtSlot(str)
      -def worker_error(self, t):
      -    LOGGER.warning("ERROR: %s" % t)
      +def puller_error(self, t): + LOGGER.warning(f"Data Puller ERROR: {t}")
      -
      -def worker_output(self, sync_id, data) +
      +def puller_output(self, sync_id, data)
      @@ -955,7 +1404,8 @@

      Methods

      Expand source code
      @pyqtSlot(int, bytes)
      -def worker_output(self, sync_id, data):
      +def puller_output(self, sync_id, data):
      +    # LOGGER.info(f"Data Puller: sync_id = {MessageIdentifier(sync_id).name}")  # Generates a lot of output!
           if sync_id == MessageIdentifier.SYNC_TIMECODE:
               timecode, timestamp = data
               # LOGGER.info(f"TIMECODE: {timecode=}, {timestamp=}")
      @@ -976,6 +1426,7 @@ 

      Methods

      v_start=self.register_map["v_start"], v_end=self.register_map["v_end"], h_end=self.register_map["h_end"], + n_fee_side=self.n_fee_side, ) self.register_box.update(self.register_map) if sync_id == MessageIdentifier.SYNC_DATA_PACKET: @@ -1021,6 +1472,62 @@

      Methods

      self.readout_params.update_par('Number of Cycles', max(0, data))
      +
      +def puller_stalled(self, flag: bool) +
      +
      +
      +
      + +Expand source code + +
      @pyqtSlot(bool)
      +def puller_stalled(self, flag: bool):
      +    params = {
      +        'N-FEE Mode': 'CONNECTION LOST',
      +        'N-FEE Cycle Time': "",
      +        'DUMP Mode': "",
      +        'Internal Sync': "",
      +        'External Sync': "",
      +    }
      +    self.mode_params.update_params(params)
      +
      +
      +
      +def start_monitoring(self) +
      +
      +
      +
      + +Expand source code + +
      def start_monitoring(self):
      +    self.data_monitor = worker = DataMonitor(DPU_SETTINGS.HOSTNAME, DPU_SETTINGS.MONITORING_PORT)
      +    self.threadpool.start(worker)
      +    worker.signals.data.connect(self.monitor_output)
      +    worker.signals.finished.connect(self.monitor_complete)
      +    worker.signals.error.connect(self.monitor_error)
      +
      +
      +
      +def start_pulling_data(self) +
      +
      +
      +
      + +Expand source code + +
      def start_pulling_data(self):
      +    self.data_puller = worker = DataPuller(DPU_SETTINGS.HOSTNAME, DPU_SETTINGS.DATA_DISTRIBUTION_PORT)
      +    self.threadpool.start(worker)
      +    worker.signals.data.connect(self.puller_output)
      +    worker.signals.finished.connect(self.puller_complete)
      +    worker.signals.error.connect(self.puller_error)
      +    worker.signals.stalled.connect(self.puller_stalled)
      +
      +
  • @@ -1028,7 +1535,7 @@

    Methods

    (params: dict)
    -

    QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -1046,7 +1553,7 @@

    Methods

    vbox.setSpacing(4) vbox.setContentsMargins(10, 0, 10, 0) - vbox.addWidget(QLabel("Mode Parameters")) + vbox.addWidget(QLabel("<b>Mode Parameters</b>")) for name, widget in self.params.items(): hbox = QHBoxLayout() @@ -1107,7 +1614,7 @@

    Methods

    (params: dict)
    -

    QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -1125,7 +1632,7 @@

    Methods

    vbox.setSpacing(4) vbox.setContentsMargins(10, 0, 10, 0) - vbox.addWidget(QLabel("Readout Parameters")) + vbox.addWidget(QLabel("<b>Readout Parameters</b>")) for name, widget in self.params.items(): hbox = QHBoxLayout() @@ -1289,6 +1796,252 @@

    Args

    +
    +class StatusParameterWidget +(params: dict) +
    +
    +

    QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +
    + +Expand source code + +
    class StatusParameterWidget(QWidget):
    +    def __init__(self, params: dict):
    +        super().__init__()
    +
    +        self.params = params
    +        self.error_flag_names = [
    +            "Wrong X-Coordinate", "Wrong Y-Coordinate", "E SRAM Full", "F SRAM Full", "AWLA Error",
    +            "SRAM EDAC Correct", "SRAM EDAC Uncorrect", "Block R EDAC",
    +            "Disconnect Error", "Escape Error", "Credit Error", "Parity Error", "Lock Error",
    +        ]
    +
    +        # Use the following line to put a small border around all the parameters
    +        # self.setStyleSheet('border: 1px solid red;')
    +
    +        vbox = QVBoxLayout()
    +        vbox.setSpacing(4)
    +        vbox.setContentsMargins(10, 0, 10, 0)
    +
    +        hbox = QHBoxLayout()
    +        hbox.setAlignment(Qt.AlignLeft)
    +        hbox.addWidget(QLabel("<b>Status Parameters</b> ("))
    +        clear_error_flags = QLabel("Clear")
    +        clear_error_flags.setStyleSheet("color: blue; text-decoration: underline")
    +        clear_error_flags.mousePressEvent = self.clear_error_flags
    +        hbox.addWidget(clear_error_flags)
    +        hbox.addWidget(QLabel(")"))
    +        vbox.addLayout(hbox)
    +
    +        for name, item in self.params.items():
    +            self.set_format(name, 0)
    +            self.update_param(name, 0)
    +            hbox = QHBoxLayout()
    +            hbox.addWidget(QLabel(name))
    +            hbox.addWidget(item['led'])
    +            label: QLabel = item['label']
    +            font = label.font()
    +            font.setStyleHint(QFont.TypeWriter)
    +            label.setFont(font)
    +            label.mousePressEvent = partial(self.toggle_format, name)
    +            hbox.addWidget(label)
    +            vbox.addLayout(hbox)
    +
    +        vbox.addStretch()
    +        self.setLayout(vbox)
    +
    +    def toggle_format(self, name, event):
    +        LOGGER.info(f"{name = }, {event = }")
    +        format_type = self.params[name]['format_type'] + 1
    +        format_type = self.params[name]['format_type'] = format_type if 0 <= format_type < 3 else 0
    +        self.set_format(name, format_type)
    +        self.refresh_param(name)
    +
    +    def set_format(self, name, format_type):
    +        LOGGER.info(f"{name = }, {format_type = }")
    +        formats = ["0x{0:04X}", "0b{0:012b}", "  {0:d}"]
    +        self.params[name]['format_type'] = format_type
    +        self.params[name]['format'] = formats[format_type]
    +
    +    def set_value(self, name, value):
    +        self.params[name]['value'] = value
    +
    +    def get_value(self, name):
    +        return self.params[name]['value']
    +
    +    def refresh_param(self, name):
    +        self.update_param(name, self.params[name]['value'])
    +
    +    def clear_error_flags(self, *args):
    +        self.update_param("Error Flags", 0, "no errors detected")
    +
    +    def update_param(self, name, value, tooltip=None):
    +        format = self.params[name]['format']
    +        self.params[name]['value'] = value
    +        self.params[name]['label'].setText(format.format(value))
    +        if tooltip:
    +            self.params[name]['label'].setToolTip(tooltip)
    +        self.params[name]['led'].set_color(Indic.BLACK if value == 0 else self.params[name]['color'])
    +
    +    def update_params(self, params: dict):
    +        LOGGER.info(f"{params = }")
    +        for name, data in params.items():
    +            if name == "Error Flags":
    +                error_flags, frame_counter, timestamp = data
    +                if self.params[name]['value'] == 0 and error_flags != 0:
    +                    self.update_param(name, error_flags, self._decode_error_flags(error_flags, frame_counter, timestamp))
    +                # self.refresh_param(name)
    +
    +    def _decode_error_flags(self, error_flags, frame_counter, timestamp) -> str:
    +
    +        if error_flags == 0:
    +            return "no errors detected"
    +
    +        msg = f"<b>Error on {frame_counter=} </b><br><br>"
    +
    +        flags = "".join([
    +            f"– {name}<br>"
    +            for idx, name in enumerate(self.error_flag_names)
    +            if bit_set(error_flags, idx)
    +        ])
    +
    +        return msg + flags
    +
    +

    Ancestors

    +
      +
    • PyQt5.QtWidgets.QWidget
    • +
    • PyQt5.QtCore.QObject
    • +
    • sip.wrapper
    • +
    • PyQt5.QtGui.QPaintDevice
    • +
    • sip.simplewrapper
    • +
    +

    Methods

    +
    +
    +def clear_error_flags(self, *args) +
    +
    +
    +
    + +Expand source code + +
    def clear_error_flags(self, *args):
    +    self.update_param("Error Flags", 0, "no errors detected")
    +
    +
    +
    +def get_value(self, name) +
    +
    +
    +
    + +Expand source code + +
    def get_value(self, name):
    +    return self.params[name]['value']
    +
    +
    +
    +def refresh_param(self, name) +
    +
    +
    +
    + +Expand source code + +
    def refresh_param(self, name):
    +    self.update_param(name, self.params[name]['value'])
    +
    +
    +
    +def set_format(self, name, format_type) +
    +
    +
    +
    + +Expand source code + +
    def set_format(self, name, format_type):
    +    LOGGER.info(f"{name = }, {format_type = }")
    +    formats = ["0x{0:04X}", "0b{0:012b}", "  {0:d}"]
    +    self.params[name]['format_type'] = format_type
    +    self.params[name]['format'] = formats[format_type]
    +
    +
    +
    +def set_value(self, name, value) +
    +
    +
    +
    + +Expand source code + +
    def set_value(self, name, value):
    +    self.params[name]['value'] = value
    +
    +
    +
    +def toggle_format(self, name, event) +
    +
    +
    +
    + +Expand source code + +
    def toggle_format(self, name, event):
    +    LOGGER.info(f"{name = }, {event = }")
    +    format_type = self.params[name]['format_type'] + 1
    +    format_type = self.params[name]['format_type'] = format_type if 0 <= format_type < 3 else 0
    +    self.set_format(name, format_type)
    +    self.refresh_param(name)
    +
    +
    +
    +def update_param(self, name, value, tooltip=None) +
    +
    +
    +
    + +Expand source code + +
    def update_param(self, name, value, tooltip=None):
    +    format = self.params[name]['format']
    +    self.params[name]['value'] = value
    +    self.params[name]['label'].setText(format.format(value))
    +    if tooltip:
    +        self.params[name]['label'].setToolTip(tooltip)
    +    self.params[name]['led'].set_color(Indic.BLACK if value == 0 else self.params[name]['color'])
    +
    +
    +
    +def update_params(self, params: dict) +
    +
    +
    +
    + +Expand source code + +
    def update_params(self, params: dict):
    +    LOGGER.info(f"{params = }")
    +    for name, data in params.items():
    +        if name == "Error Flags":
    +            error_flags, frame_counter, timestamp = data
    +            if self.params[name]['value'] == 0 and error_flags != 0:
    +                self.update_param(name, error_flags, self._decode_error_flags(error_flags, frame_counter, timestamp))
    +            # self.refresh_param(name)
    +
    +
    +
    +
    class WorkerSignals (*args, **kwargs) @@ -1325,7 +2078,8 @@

    Args

    finished = pyqtSignal() error = pyqtSignal(str) - data = pyqtSignal(int, object)
    + data = pyqtSignal(int, object) + stalled = pyqtSignal(bool)

    Ancestors

      @@ -1353,6 +2107,12 @@

      Methods

      +
      +def stalled(...) +
      +
      +
      +
      @@ -1377,6 +2137,13 @@

      Index

    • Classes

      diff --git a/docs/api/egse/dpu/fitsgen.html b/docs/api/egse/dpu/fitsgen.html index a0da659..842ed1f 100644 --- a/docs/api/egse/dpu/fitsgen.html +++ b/docs/api/egse/dpu/fitsgen.html @@ -43,11 +43,18 @@

      Module egse.dpu.fitsgen

      import multiprocessing import os import pickle +import sys import threading -from datetime import timedelta, datetime +import time +from datetime import datetime +from datetime import timedelta from enum import Enum -from pathlib import Path, PosixPath -from typing import List, Mapping +from enum import EnumMeta +from itertools import chain +from pathlib import Path +from pathlib import PosixPath +from typing import List +from typing import Mapping import click import invoke @@ -55,39 +62,46 @@

      Module egse.dpu.fitsgen

      import numpy as np import persistqueue import rich -import sys -import time import zmq from astropy.io import fits from h5py import File from h5py._hl.attrs import AttributeManager -from itertools import chain from scipy.interpolate import interp1d import egse from egse import h5 -from egse.config import find_file, find_files +from egse.config import find_file +from egse.config import find_files from egse.control import time_in_ms -from egse.dpu import DPUMonitoring, get_expected_last_packet_flags +from egse.dpu import DPUMonitoring +from egse.dpu import get_expected_last_packet_flags from egse.dpu.dpu_cs import is_dpu_cs_active from egse.env import get_data_storage_location from egse.exceptions import Abort from egse.fee import convert_ccd_order_value from egse.fee import n_fee_mode from egse.fee.nfee import HousekeepingData -from egse.hk import get_housekeeping, HKError -from egse.obsid import ObservationIdentifier, LAB_SETUP_TEST, TEST_LAB, obsid_from_storage +from egse.hk import HKError +from egse.hk import get_housekeeping +from egse.obsid import LAB_SETUP_TEST +from egse.obsid import ObservationIdentifier +from egse.obsid import TEST_LAB +from egse.obsid import obsid_from_storage from egse.reg import RegisterMap from egse.settings import Settings -from egse.setup import load_setup, Setup +from egse.setup import Setup +from egse.setup import load_setup from egse.spw import SpaceWirePacket -from egse.state import GlobalState from egse.storage import is_storage_manager_active -from egse.storage.persistence import FITS, HDF5 +from egse.storage.persistence import FITS +from egse.storage.persistence import HDF5 from egse.synoptics import ORIGIN as SYN_ORIGIN from egse.synoptics import get_synoptics_table -from egse.system import time_since_epoch_1958, format_datetime, read_last_line -from egse.zmq_ser import bind_address, connect_address +from egse.system import format_datetime +from egse.system import read_last_line +from egse.system import time_since_epoch_1958 +from egse.zmq_ser import bind_address +from egse.zmq_ser import connect_address LOGGER = logging.getLogger(__name__) @@ -190,8 +204,9 @@

      Module egse.dpu.fitsgen

      self.ccd_readout_order = None # self.sensor_sel = None self.rows_final_dump = None - self.setup = GlobalState.setup + self.setup = load_setup() self.sensor_sel_enum = self.setup.camera.fee.sensor_sel.enum + self.fee_side = self.setup.camera.fee.ccd_sides.enum self.camera_name = self.setup.camera.ID self.config_slicing_num_cycles = 0 # Configured slicing parameter @@ -322,7 +337,7 @@

      Module egse.dpu.fitsgen

      location = get_data_storage_location() - previous_obsid = None + syn_obsid = None while self.keep_processing_queue: @@ -458,6 +473,7 @@

      Module egse.dpu.fitsgen

      if has_data: self.processed_num_cycles += 1 + syn_obsid = obsid if self.config_slicing_num_cycles != 0 and \ self.processed_num_cycles == self.config_slicing_num_cycles: @@ -472,9 +488,10 @@

      Module egse.dpu.fitsgen

      # it means that the observation has just finished and all FITS files have been generated. It # is only at this point that the synoptics can be included in the FITS headers. - if previous_obsid is not None and obsid is None: - add_synoptics(previous_obsid, fits_dir=location, syn_dir=location) - previous_obsid = obsid + if syn_obsid is not None and obsid is None: + LOGGER.info(f"Adding synoptics for {syn_obsid}") + add_synoptics(syn_obsid, fits_dir=location, syn_dir=location, fee_side=self.fee_side) + syn_obsid = None except KeyError: LOGGER.debug("KeyError occurred when accessing data in all groups of the HDF5 file.") @@ -498,7 +515,7 @@

      Module egse.dpu.fitsgen

      if self.fits_images_filename is not None: self.fits_cube_filename = construct_cube_filename(self.fits_images_filename) - convert_to_cubes(self.fits_images_filename) + convert_to_cubes(self.fits_images_filename, self.setup) self.fits_cube_filename = None # Stop writing to the current FITS file @@ -580,7 +597,7 @@

      Module egse.dpu.fitsgen

      return crucial_parameter_change -def convert_to_cubes(filename): +def convert_to_cubes(filename, setup: Setup): """ Conversion of level-1 FITS files to level-2 FITS files. After the conversion, the flat-structure FITS file is removed. @@ -589,9 +606,9 @@

      Module egse.dpu.fitsgen

      - filename: Full path of the level-1 FITS file. """ + fee_side = setup.camera.fee.ccd_sides.enum cube_filename = construct_cube_filename(filename) LOGGER.info(f"Converting to {cube_filename}") - fee_side = GlobalState.setup.camera.fee.ccd_sides.enum with fits.open(filename) as level1: @@ -945,8 +962,9 @@

      Module egse.dpu.fitsgen

      obsid = obsid_from_storage(obsid, data_dir=location, camera_name=camera_name) - timestamp = str.split(str(hdf5_filename).split("/")[-1], "_")[0] location += "/obs/" + dpu_filename = find_file(f"{obsid}_DPU_*.csv", root=f"{hdf5_filename.parents[2]}/obs/{obsid}") + timestamp = str(dpu_filename).split("_")[-2] if not os.path.isdir(f"{location}/{obsid}"): os.makedirs(f"{location}/{obsid}") @@ -1108,7 +1126,7 @@

      Module egse.dpu.fitsgen

      environment variable will be used to construct the location. - setup: Setup to retrieve information from. """ - setup = setup or GlobalState.setup + setup = setup or load_setup() location = location or get_data_storage_location() hdf5_file_root = Path(files[0]).parent.parent.parent @@ -1156,7 +1174,7 @@

      Module egse.dpu.fitsgen

      if fits_filename: LOGGER.info(f"Creating a FITS CUBE file ...") - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) fits_filename = None if in_data_acquisition_mode(register_map): @@ -1225,7 +1243,7 @@

      Module egse.dpu.fitsgen

      if fits_filename: LOGGER.info(f"Creating a FITS CUBE file ...") - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) fits_filename = None prep = clear_crucial_parameters(prep) @@ -1238,7 +1256,7 @@

      Module egse.dpu.fitsgen

      try: if fits_filename: LOGGER.info(f"Creating a FITS CUBE file ...") - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) except OSError: # The last file in the list still contained data, so we reached the end of the list without creating a cube # FITS file yet @@ -1263,11 +1281,12 @@

      Module egse.dpu.fitsgen

      Args: - files: List of filenames of the HDF5 files to use to create the FITS file. - location: Folder (with /daily and /obs sub-folders) in which the FITS files should be written (in a - dedicated directory in the /obs folder). If not specified, the `PLATO_DATA_STORAGE_LOCATION` - environment variable will be used to construct the location. - - setup: Setup to retrieve information from. + dedicated directory in the /obs folder). If not specified, the `PLATO_DATA_STORAGE_LOCATION` + environment variable will be used to construct the location. + - setup: Setup to retrieve information from, if not provided, the setup is loaded from the + configuration manager.. """ - setup = setup or GlobalState.setup + setup = setup or load_setup() location = location or get_data_storage_location() hdf5_file_root = Path(files[0]).parent.parent.parent @@ -1298,7 +1317,7 @@

      Module egse.dpu.fitsgen

      slicing_num_cycles = hdf5_file["dpu"].attrs["slicing_num_cycles"] if slicing_num_cycles != config_slicing_num_cycles: if fits_filename: - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) fits_filename = None processed_num_cycles = 0 config_slicing_num_cycles = slicing_num_cycles @@ -1329,7 +1348,7 @@

      Module egse.dpu.fitsgen

      if fits_filename: LOGGER.info(f"Creating a FITS CUBE file ...") - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) fits_filename = None processed_num_cycles = 0 @@ -1399,14 +1418,14 @@

      Module egse.dpu.fitsgen

      if fits_filename and config_slicing_num_cycles != 0 \ and processed_num_cycles == config_slicing_num_cycles: - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) fits_filename = None processed_num_cycles = 0 else: if fits_filename: LOGGER.info(f"Creating a FITS CUBE file ...") - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) fits_filename = None processed_num_cycles = 0 @@ -1420,7 +1439,7 @@

      Module egse.dpu.fitsgen

      try: if fits_filename: LOGGER.info(f"Creating a FITS CUBE file ...") - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) except OSError: # The last file in the list still contained data, so we reached the end of the list without creating a cube # FITS file yet @@ -1483,13 +1502,18 @@

      Module egse.dpu.fitsgen

      T_TRP31 = ("GSYN_TRP31", "Mean T for TRP31 (TOU bipod -Y bottom) [deg C]") T_TRP41 = ("GSYN_TRP41", "Mean T for TRP41 (TOU bipod +Y bottom) [deg C]") - # CCD PT100 sensors (N-FEE) + # CCD PT100/PT1000 sensors (N-FEE) T_CCD1 = ("GSYN_CCD1", "Mean T for CCD1 [deg C]") T_CCD2 = ("GSYN_CCD2", "Mean T for CCD2 [deg C]") T_CCD3 = ("GSYN_CCD3", "Mean T for CCD3 [deg C]") T_CCD4 = ("GSYN_CCD4", "Mean T for CCD4 [deg C]") + T_CCD1_AMB = ("GSYN_CCD1_AMB", "Mean T for CCD1 (ambient calibration) [deg C]") + T_CCD2_AMB = ("GSYN_CCD2_AMB", "Mean T for CCD2 (ambient calibration) [deg C]") + T_CCD3_AMB = ("GSYN_CCD3_AMB", "Mean T for CCD3 (ambient calibration) [deg C]") + T_CCD4_AMB = ("GSYN_CCD4_AMB", "Mean T for CCD4 (ambient calibration) [deg C]") + # Board sensors: type PT1000 (N-FEE) T_PCB1 = ("GSYN_NFEE_T_PCB1", "Mean T for board sensor PCB1 [deg C]") @@ -1539,7 +1563,7 @@

      Module egse.dpu.fitsgen

      def get_fits_synoptics(obsid: str, data_dir=None) -> dict: """ Retrieve the synoptics that need to be included in the FITS files for the given observation. - The synoptics that need be be included in the FITS files are represented by the following enumerations: + The synoptics that need to be included in the FITS files are represented by the following enumerations: - SynopticsFwdFill: Use forward filling for the gaps -> only at the beginning of the observation it is possible that there still are gaps (but it is unlikely that the data acquisition has already started then); @@ -1579,9 +1603,13 @@

      Module egse.dpu.fitsgen

      selection = ~np.isnan(values) if np.any(selection): - interpolation = interp1d(timestamps[np.where(selection)], values[np.where(selection)], kind='linear', - fill_value='extrapolate') - synoptics[syn_enum] = interpolation(timestamps) + selected_timestamps = timestamps[np.where(selection)] + selected_values = values[np.where(selection)] + + if len(selected_timestamps) > 1: + interpolation = interp1d(selected_timestamps, selected_values, kind='linear', + fill_value='extrapolate') + synoptics[syn_enum] = interpolation(timestamps) # Forward fill @@ -1604,7 +1632,7 @@

      Module egse.dpu.fitsgen

      return synoptics -def add_synoptics(obsid: str, fits_dir: str, syn_dir: str): +def add_synoptics(obsid: str, fits_dir: str, syn_dir: str, fee_side: EnumMeta): """ Add synoptics to the FITS headers for the given observation. When all FITS files have been produced for the given obsid, synoptics is added to the headers. This is done in the @@ -1623,11 +1651,11 @@

      Module egse.dpu.fitsgen

      obsid: Observation identifier [TEST_LAB or TEST_LAB_SETUP] fits_dir: Directory (with /daily and /obs sub-folders) with the FITS files syn_dir: Directory (with /daily and /obs sub-folders) with the original synoptics files + fee_side: Enumeration with the definition of the FEE CCD sides """ fits_dir = fits_dir or get_data_storage_location() syn_dir = syn_dir or get_data_storage_location() - fee_side = GlobalState.setup.camera.fee.ccd_sides.enum obsid = obsid_from_storage(obsid, data_dir=fits_dir) obs_dir = f"{fits_dir}/obs/{obsid}" # Where the HK and FITS files are stored @@ -1941,14 +1969,16 @@

      Module egse.dpu.fitsgen

      # Create FITS files (flat structure -> cubes) create_fits_from_hdf5(hdf5_filenames, location=output_dir, setup=setup) + fee_side = setup.camera.fee.ccd_sides.enum + # Add synoptics - if find_file(f"{obsid_from_storage(obsid, data_dir=input_dir)}_{SYN_ORIGIN}d_*.csv", root=output_obs_folder): + if find_file(f"{obsid_from_storage(obsid, data_dir=input_dir)}_{SYN_ORIGIN}_*.csv", root=output_obs_folder): # Synoptics have already been re-processed (located in the directory to which the FITS files will be stored) - add_synoptics(obsid, fits_dir=output_dir, syn_dir=output_dir) + add_synoptics(obsid, fits_dir=output_dir, syn_dir=output_dir, fee_side=fee_side) else: # Use the original synoptics files - add_synoptics(obsid, fits_dir=output_dir, syn_dir=input_dir) + add_synoptics(obsid, fits_dir=output_dir, syn_dir=input_dir, fee_side=fee_side) def get_offline_setup(site_id: str = None, setup_id: int = None): @@ -1966,7 +1996,7 @@

      Module egse.dpu.fitsgen

      """ if setup_id is None: - return GlobalState.setup + return load_setup() else: site_id = site_id or SITE.ID return load_setup(setup_id=setup_id, site_id=site_id, from_disk=True) @@ -2106,7 +2136,7 @@

      Module egse.dpu.fitsgen

      Functions

      -def add_synoptics(obsid: str, fits_dir: str, syn_dir: str) +def add_synoptics(obsid: str, fits_dir: str, syn_dir: str, fee_side: enum.EnumMeta)

      Add synoptics to the FITS headers for the given observation.

      @@ -2130,12 +2160,14 @@

      Args

      Directory (with /daily and /obs sub-folders) with the FITS files
      syn_dir
      Directory (with /daily and /obs sub-folders) with the original synoptics files
      +
      fee_side
      +
      Enumeration with the definition of the FEE CCD sides
      Expand source code -
      def add_synoptics(obsid: str, fits_dir: str, syn_dir: str):
      +
      def add_synoptics(obsid: str, fits_dir: str, syn_dir: str, fee_side: EnumMeta):
           """ Add synoptics to the FITS headers for the given observation.
       
           When all FITS files have been produced for the given obsid, synoptics is added to the headers.  This is done in the
      @@ -2154,11 +2186,11 @@ 

      Args

      obsid: Observation identifier [TEST_LAB or TEST_LAB_SETUP] fits_dir: Directory (with /daily and /obs sub-folders) with the FITS files syn_dir: Directory (with /daily and /obs sub-folders) with the original synoptics files + fee_side: Enumeration with the definition of the FEE CCD sides """ fits_dir = fits_dir or get_data_storage_location() syn_dir = syn_dir or get_data_storage_location() - fee_side = GlobalState.setup.camera.fee.ccd_sides.enum obsid = obsid_from_storage(obsid, data_dir=fits_dir) obs_dir = f"{fits_dir}/obs/{obsid}" # Where the HK and FITS files are stored @@ -2472,8 +2504,9 @@

      Args

      obsid = obsid_from_storage(obsid, data_dir=location, camera_name=camera_name) - timestamp = str.split(str(hdf5_filename).split("/")[-1], "_")[0] location += "/obs/" + dpu_filename = find_file(f"{obsid}_DPU_*.csv", root=f"{hdf5_filename.parents[2]}/obs/{obsid}") + timestamp = str(dpu_filename).split("_")[-2] if not os.path.isdir(f"{location}/{obsid}"): os.makedirs(f"{location}/{obsid}") @@ -2490,7 +2523,7 @@

      Args

      -def convert_to_cubes(filename) +def convert_to_cubes(filename, setup: Setup)

      Conversion of level-1 FITS files to level-2 FITS files.

      @@ -2503,7 +2536,7 @@

      Args

      Expand source code -
      def convert_to_cubes(filename):
      +
      def convert_to_cubes(filename, setup: Setup):
           """ Conversion of level-1 FITS files to level-2 FITS files.
       
           After the conversion, the flat-structure FITS file is removed.
      @@ -2512,9 +2545,9 @@ 

      Args

      - filename: Full path of the level-1 FITS file. """ + fee_side = setup.camera.fee.ccd_sides.enum cube_filename = construct_cube_filename(filename) LOGGER.info(f"Converting to {cube_filename}") - fee_side = GlobalState.setup.camera.fee.ccd_sides.enum with fits.open(filename) as level1: @@ -2847,7 +2880,8 @@

      Args

    • location: Folder (with /daily and /obs sub-folders) in which the FITS files should be written (in a dedicated directory in the /obs folder). If not specified, the PLATO_DATA_STORAGE_LOCATION environment variable will be used to construct the location.
    • -
    • setup: Setup to retrieve information from.
    • +
    • setup: Setup to retrieve information from, if not provided, the setup is loaded from the +configuration manager..
    @@ -2871,11 +2905,12 @@

    Args

    Args: - files: List of filenames of the HDF5 files to use to create the FITS file. - location: Folder (with /daily and /obs sub-folders) in which the FITS files should be written (in a - dedicated directory in the /obs folder). If not specified, the `PLATO_DATA_STORAGE_LOCATION` - environment variable will be used to construct the location. - - setup: Setup to retrieve information from. + dedicated directory in the /obs folder). If not specified, the `PLATO_DATA_STORAGE_LOCATION` + environment variable will be used to construct the location. + - setup: Setup to retrieve information from, if not provided, the setup is loaded from the + configuration manager.. """ - setup = setup or GlobalState.setup + setup = setup or load_setup() location = location or get_data_storage_location() hdf5_file_root = Path(files[0]).parent.parent.parent @@ -2906,7 +2941,7 @@

    Args

    slicing_num_cycles = hdf5_file["dpu"].attrs["slicing_num_cycles"] if slicing_num_cycles != config_slicing_num_cycles: if fits_filename: - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) fits_filename = None processed_num_cycles = 0 config_slicing_num_cycles = slicing_num_cycles @@ -2937,7 +2972,7 @@

    Args

    if fits_filename: LOGGER.info(f"Creating a FITS CUBE file ...") - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) fits_filename = None processed_num_cycles = 0 @@ -3007,14 +3042,14 @@

    Args

    if fits_filename and config_slicing_num_cycles != 0 \ and processed_num_cycles == config_slicing_num_cycles: - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) fits_filename = None processed_num_cycles = 0 else: if fits_filename: LOGGER.info(f"Creating a FITS CUBE file ...") - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) fits_filename = None processed_num_cycles = 0 @@ -3028,7 +3063,7 @@

    Args

    try: if fits_filename: LOGGER.info(f"Creating a FITS CUBE file ...") - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) except OSError: # The last file in the list still contained data, so we reached the end of the list without creating a cube # FITS file yet @@ -3091,7 +3126,7 @@

    Args

    environment variable will be used to construct the location. - setup: Setup to retrieve information from. """ - setup = setup or GlobalState.setup + setup = setup or load_setup() location = location or get_data_storage_location() hdf5_file_root = Path(files[0]).parent.parent.parent @@ -3139,7 +3174,7 @@

    Args

    if fits_filename: LOGGER.info(f"Creating a FITS CUBE file ...") - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) fits_filename = None if in_data_acquisition_mode(register_map): @@ -3208,7 +3243,7 @@

    Args

    if fits_filename: LOGGER.info(f"Creating a FITS CUBE file ...") - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) fits_filename = None prep = clear_crucial_parameters(prep) @@ -3221,7 +3256,7 @@

    Args

    try: if fits_filename: LOGGER.info(f"Creating a FITS CUBE file ...") - convert_to_cubes(fits_filename) + convert_to_cubes(fits_filename, setup) except OSError: # The last file in the list still contained data, so we reached the end of the list without creating a cube # FITS file yet @@ -3375,7 +3410,7 @@

    Args

    Retrieve the synoptics that need to be included in the FITS files for the given observation.

    -

    The synoptics that need be be included in the FITS files are represented by the following enumerations:

    +

    The synoptics that need to be included in the FITS files are represented by the following enumerations:

    - SynopticsFwdFill: Use forward filling for the gaps -> only at the beginning of the observation it is possible
       that there still are gaps (but it is unlikely that the data acquisition has already started then);
     - SynopticsInterp1d: Use linear interpolation to fill the gaps.  At the extremes, we use extrapolation;
    @@ -3393,7 +3428,7 @@ 

    Args

    def get_fits_synoptics(obsid: str, data_dir=None) -> dict:
         """ Retrieve the synoptics that need to be included in the FITS files for the given observation.
     
    -    The synoptics that need be be included in the FITS files are represented by the following enumerations:
    +    The synoptics that need to be included in the FITS files are represented by the following enumerations:
     
             - SynopticsFwdFill: Use forward filling for the gaps -> only at the beginning of the observation it is possible
               that there still are gaps (but it is unlikely that the data acquisition has already started then);
    @@ -3433,9 +3468,13 @@ 

    Args

    selection = ~np.isnan(values) if np.any(selection): - interpolation = interp1d(timestamps[np.where(selection)], values[np.where(selection)], kind='linear', - fill_value='extrapolate') - synoptics[syn_enum] = interpolation(timestamps) + selected_timestamps = timestamps[np.where(selection)] + selected_values = values[np.where(selection)] + + if len(selected_timestamps) > 1: + interpolation = interp1d(selected_timestamps, selected_values, kind='linear', + fill_value='extrapolate') + synoptics[syn_enum] = interpolation(timestamps) # Forward fill @@ -3686,7 +3725,7 @@

    Returns

    """ if setup_id is None: - return GlobalState.setup + return load_setup() else: site_id = site_id or SITE.ID return load_setup(setup_id=setup_id, site_id=site_id, from_disk=True)
    @@ -3894,8 +3933,9 @@

    Classes

    self.ccd_readout_order = None # self.sensor_sel = None self.rows_final_dump = None - self.setup = GlobalState.setup + self.setup = load_setup() self.sensor_sel_enum = self.setup.camera.fee.sensor_sel.enum + self.fee_side = self.setup.camera.fee.ccd_sides.enum self.camera_name = self.setup.camera.ID self.config_slicing_num_cycles = 0 # Configured slicing parameter @@ -4026,7 +4066,7 @@

    Classes

    location = get_data_storage_location() - previous_obsid = None + syn_obsid = None while self.keep_processing_queue: @@ -4162,6 +4202,7 @@

    Classes

    if has_data: self.processed_num_cycles += 1 + syn_obsid = obsid if self.config_slicing_num_cycles != 0 and \ self.processed_num_cycles == self.config_slicing_num_cycles: @@ -4176,9 +4217,10 @@

    Classes

    # it means that the observation has just finished and all FITS files have been generated. It # is only at this point that the synoptics can be included in the FITS headers. - if previous_obsid is not None and obsid is None: - add_synoptics(previous_obsid, fits_dir=location, syn_dir=location) - previous_obsid = obsid + if syn_obsid is not None and obsid is None: + LOGGER.info(f"Adding synoptics for {syn_obsid}") + add_synoptics(syn_obsid, fits_dir=location, syn_dir=location, fee_side=self.fee_side) + syn_obsid = None except KeyError: LOGGER.debug("KeyError occurred when accessing data in all groups of the HDF5 file.") @@ -4202,7 +4244,7 @@

    Classes

    if self.fits_images_filename is not None: self.fits_cube_filename = construct_cube_filename(self.fits_images_filename) - convert_to_cubes(self.fits_images_filename) + convert_to_cubes(self.fits_images_filename, self.setup) self.fits_cube_filename = None # Stop writing to the current FITS file @@ -4319,7 +4361,7 @@

    Methods

    if self.fits_images_filename is not None: self.fits_cube_filename = construct_cube_filename(self.fits_images_filename) - convert_to_cubes(self.fits_images_filename) + convert_to_cubes(self.fits_images_filename, self.setup) self.fits_cube_filename = None # Stop writing to the current FITS file @@ -4476,7 +4518,7 @@

    Args

    location = get_data_storage_location() - previous_obsid = None + syn_obsid = None while self.keep_processing_queue: @@ -4612,6 +4654,7 @@

    Args

    if has_data: self.processed_num_cycles += 1 + syn_obsid = obsid if self.config_slicing_num_cycles != 0 and \ self.processed_num_cycles == self.config_slicing_num_cycles: @@ -4626,9 +4669,10 @@

    Args

    # it means that the observation has just finished and all FITS files have been generated. It # is only at this point that the synoptics can be included in the FITS headers. - if previous_obsid is not None and obsid is None: - add_synoptics(previous_obsid, fits_dir=location, syn_dir=location) - previous_obsid = obsid + if syn_obsid is not None and obsid is None: + LOGGER.info(f"Adding synoptics for {syn_obsid}") + add_synoptics(syn_obsid, fits_dir=location, syn_dir=location, fee_side=self.fee_side) + syn_obsid = None except KeyError: LOGGER.debug("KeyError occurred when accessing data in all groups of the HDF5 file.") @@ -4807,13 +4851,18 @@

    Class variables

    T_TRP31 = ("GSYN_TRP31", "Mean T for TRP31 (TOU bipod -Y bottom) [deg C]") T_TRP41 = ("GSYN_TRP41", "Mean T for TRP41 (TOU bipod +Y bottom) [deg C]") - # CCD PT100 sensors (N-FEE) + # CCD PT100/PT1000 sensors (N-FEE) T_CCD1 = ("GSYN_CCD1", "Mean T for CCD1 [deg C]") T_CCD2 = ("GSYN_CCD2", "Mean T for CCD2 [deg C]") T_CCD3 = ("GSYN_CCD3", "Mean T for CCD3 [deg C]") T_CCD4 = ("GSYN_CCD4", "Mean T for CCD4 [deg C]") + T_CCD1_AMB = ("GSYN_CCD1_AMB", "Mean T for CCD1 (ambient calibration) [deg C]") + T_CCD2_AMB = ("GSYN_CCD2_AMB", "Mean T for CCD2 (ambient calibration) [deg C]") + T_CCD3_AMB = ("GSYN_CCD3_AMB", "Mean T for CCD3 (ambient calibration) [deg C]") + T_CCD4_AMB = ("GSYN_CCD4_AMB", "Mean T for CCD4 (ambient calibration) [deg C]") + # Board sensors: type PT1000 (N-FEE) T_PCB1 = ("GSYN_NFEE_T_PCB1", "Mean T for board sensor PCB1 [deg C]") @@ -4884,18 +4933,34 @@

    Class variables

    +
    var T_CCD1_AMB
    +
    +
    +
    var T_CCD2
    +
    var T_CCD2_AMB
    +
    +
    +
    var T_CCD3
    +
    var T_CCD3_AMB
    +
    +
    +
    var T_CCD4
    +
    var T_CCD4_AMB
    +
    +
    +
    var T_CDS
    @@ -5086,9 +5151,13 @@

    T_ADC
  • T_ANALOG
  • T_CCD1
  • +
  • T_CCD1_AMB
  • T_CCD2
  • +
  • T_CCD2_AMB
  • T_CCD3
  • +
  • T_CCD3_AMB
  • T_CCD4
  • +
  • T_CCD4_AMB
  • T_CDS
  • T_PCB1
  • T_PCB2
  • diff --git a/docs/api/egse/dpu/fitsgen_ui.html b/docs/api/egse/dpu/fitsgen_ui.html index e1c13c5..3b1ea2f 100644 --- a/docs/api/egse/dpu/fitsgen_ui.html +++ b/docs/api/egse/dpu/fitsgen_ui.html @@ -817,7 +817,7 @@

    Class variables

    class FitsgenUIView
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Open a window and initialise the GUI.

    The following information is shown: - Status of the FITS generation process; diff --git a/docs/api/egse/dpu/hdf5_model.html b/docs/api/egse/dpu/hdf5_model.html index 73f3181..4a545b4 100644 --- a/docs/api/egse/dpu/hdf5_model.html +++ b/docs/api/egse/dpu/hdf5_model.html @@ -727,7 +727,7 @@

    Inherited members

    (reg_map: RegisterMap)
    -

    QAbstractTableModel(parent: QObject = None)

    +

    QAbstractTableModel(parent: typing.Optional[QObject] = None)

    Expand source code @@ -802,7 +802,7 @@

    Methods

    def data(self, index, role)
    -

    data(self, QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    +

    data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    Expand source code @@ -825,7 +825,7 @@

    Methods

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = None) ‑> Any
    -

    headerData(self, int, Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    +

    headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    Expand source code @@ -1250,7 +1250,7 @@

    Methods

    def data(self, index: QModelIndex, role: int = Ellipsis) ‑> Any
    -

    data(self, QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    +

    data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    Expand source code @@ -1272,7 +1272,7 @@

    Methods

    def flags(self, index: QModelIndex) ‑> PyQt5.QtCore.Qt.ItemFlags
    -

    flags(self, QModelIndex) -> Qt.ItemFlags

    +

    flags(self, index: QModelIndex) -> Qt.ItemFlags

    Expand source code @@ -1306,7 +1306,7 @@

    Methods

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = Ellipsis) ‑> Any
    -

    headerData(self, int, Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    +

    headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    Expand source code @@ -1321,7 +1321,7 @@

    Methods

    def index(self, row: int, column: int, parent: QModelIndex = Ellipsis) ‑> PyQt5.QtCore.QModelIndex
    -

    index(self, int, int, parent: QModelIndex = QModelIndex()) -> QModelIndex

    +

    index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex

    Expand source code @@ -1348,8 +1348,8 @@

    Methods

    def parent(self, child: QModelIndex) ‑> PyQt5.QtCore.QModelIndex
    -

    parent(self, QModelIndex) -> QModelIndex -parent(self) -> QObject

    +

    parent(self, child: QModelIndex) -> QModelIndex +parent(self) -> typing.Optional[QObject]

    Expand source code diff --git a/docs/api/egse/dpu/hdf5_ui.html b/docs/api/egse/dpu/hdf5_ui.html index 976dd3c..c573d12 100644 --- a/docs/api/egse/dpu/hdf5_ui.html +++ b/docs/api/egse/dpu/hdf5_ui.html @@ -142,7 +142,7 @@

    Module egse.dpu.hdf5_ui

    self.hk_box = NFEEHousekeepingWidget() - self.graph_box = NFEECCDWidget(ccd_number=None, maximize=False) + self.graph_box = NFEECCDWidget(ccd_number=None, n_fee_side=main_window.n_fee_side, maximize=False) self.data_view: QStackedWidget = QStackedWidget(splitter) self.data_view.addWidget(self.text_box) @@ -322,7 +322,7 @@

    Classes

    (data)
    -

    QAbstractTableModel(parent: QObject = None)

    +

    QAbstractTableModel(parent: typing.Optional[QObject] = None)

    Args

    data
    @@ -388,7 +388,7 @@

    Methods

    def data(self, index, role)
    -

    data(self, QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    +

    data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    Expand source code @@ -402,7 +402,7 @@

    Methods

    def headerData(self, section, orientation, role)
    -

    headerData(self, int, Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    +

    headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    Expand source code @@ -436,7 +436,7 @@

    Methods

    (data: DataPacket)
    -

    QAbstractTableModel(parent: QObject = None)

    +

    QAbstractTableModel(parent: typing.Optional[QObject] = None)

    Args

    data : DataPacket
    @@ -502,7 +502,7 @@

    Methods

    def data(self, index, role)
    -

    data(self, QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    +

    data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    Expand source code @@ -516,7 +516,7 @@

    Methods

    def headerData(self, section, orientation, role)
    -

    headerData(self, int, Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    +

    headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    Expand source code @@ -550,7 +550,7 @@

    Methods

    (data: h5py._hl.dataset.Dataset)
    -

    QAbstractTableModel(parent: QObject = None)

    +

    QAbstractTableModel(parent: typing.Optional[QObject] = None)

    Args

    data
    @@ -604,7 +604,7 @@

    Inherited members

    (data: h5py._hl.files.File)
    -

    QAbstractTableModel(parent: QObject = None)

    +

    QAbstractTableModel(parent: typing.Optional[QObject] = None)

    Args

    data
    @@ -653,7 +653,7 @@

    Inherited members

    (data: h5py._hl.group.Group)
    -

    QAbstractTableModel(parent: QObject = None)

    +

    QAbstractTableModel(parent: typing.Optional[QObject] = None)

    Args

    data
    @@ -781,7 +781,7 @@

    Inherited members

    self.hk_box = NFEEHousekeepingWidget() - self.graph_box = NFEECCDWidget(ccd_number=None, maximize=False) + self.graph_box = NFEECCDWidget(ccd_number=None, n_fee_side=main_window.n_fee_side, maximize=False) self.data_view: QStackedWidget = QStackedWidget(splitter) self.data_view.addWidget(self.text_box) @@ -912,7 +912,7 @@

    Args

    self.hk_box = NFEEHousekeepingWidget() - self.graph_box = NFEECCDWidget(ccd_number=None, maximize=False) + self.graph_box = NFEECCDWidget(ccd_number=None, n_fee_side=main_window.n_fee_side, maximize=False) self.data_view: QStackedWidget = QStackedWidget(splitter) self.data_view.addWidget(self.text_box) @@ -948,7 +948,7 @@

    Args

    class PropertiesModel
    -

    QAbstractTableModel(parent: QObject = None)

    +

    QAbstractTableModel(parent: typing.Optional[QObject] = None)

    Expand source code @@ -1014,7 +1014,7 @@

    Methods

    def data(self, index, role)
    -

    data(self, QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    +

    data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    Expand source code @@ -1032,7 +1032,7 @@

    Methods

    def headerData(self, section, orientation, role)
    -

    headerData(self, int, Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    +

    headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any

    Expand source code diff --git a/docs/api/egse/dpu/hdf5_viewer.html b/docs/api/egse/dpu/hdf5_viewer.html index 545309a..3902b8b 100644 --- a/docs/api/egse/dpu/hdf5_viewer.html +++ b/docs/api/egse/dpu/hdf5_viewer.html @@ -92,10 +92,10 @@

    Module egse.dpu.hdf5_viewer

    from egse.fee.nfee import HousekeepingData from egse.reg import RegisterMap from egse.setup import SetupError +from egse.setup import load_setup from egse.spw import DataPacket from egse.spw import HousekeepingPacket from egse.spw import SpaceWirePacket -from egse.state import GlobalState MODULE_LOGGER = logging.getLogger(__name__) @@ -155,7 +155,9 @@

    Module egse.dpu.hdf5_viewer

    def __init__(self): super().__init__() - self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum + setup = load_setup() + self.n_fee_side = setup.camera.fee.ccd_sides.enum + self.ccd_bin_to_id = setup.camera.fee.ccd_numbering.CCD_BIN_TO_ID # Setup the complete GUI @@ -233,6 +235,9 @@

    Module egse.dpu.hdf5_viewer

    self.data_view.setCurrentWidget(self.table_box) self.table_view.resizeColumnsToContents() self.proxy_model.layoutChanged.emit() + elif "hk_data" in hdf_item.name: + packet = HousekeepingData(hdf_item[()]) + self.display_hk_data_text(packet) elif "hk" in hdf_item.name: packet = HousekeepingPacket(hdf_item[()]) self.display_hk_packet_view(packet) @@ -251,6 +256,15 @@

    Module egse.dpu.hdf5_viewer

    if "data" in hdf_item: self.handle_image_data(hdf_item) + def display_hk_data_text(self, hk_data: HousekeepingData): + + console = Console(width=120, force_terminal=False, force_jupyter=False) + with console.capture() as capture: + console.print(hk_data) + text = capture.get() + self.text_box.setText(text) + self.data_view.setCurrentWidget(self.text_box) + def display_hk_packet_view(self, packet: HousekeepingPacket): hk_data = HousekeepingData(packet.data) @@ -314,7 +328,7 @@

    Module egse.dpu.hdf5_viewer

    # Create the Worker, any other args, kwargs are passed to the run function worker = Worker( - create_image, data_item, v_start, v_end, h_end + create_image, data_item, v_start, v_end, h_end, self.n_fee_side, self.ccd_bin_to_id, ) worker.signals.result.connect(self.show_image_data) worker.signals.finished.connect(self.worker_finished) @@ -401,17 +415,15 @@

    Module egse.dpu.hdf5_viewer

    # @profile(sort_by='cumulative', lines_to_print=10, strip_dirs=True) -def create_image(hdf_item: h5py.Group, v_start: int, v_end: int, h_end: int, +def create_image(hdf_item: h5py.Group, v_start: int, v_end: int, h_end: int, n_fee_side, ccd_bin_to_id, signals: WorkerSignals): - n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum - # MODULE_LOGGER.info(f"Creating image: {v_start=}, {v_end=}") nr_lines = v_end - v_start + 1 nr_columns = h_end + 1 - image = ImageCreatorFullSize(v_start, v_end, h_end) + image = ImageCreatorFullSize(v_start, v_end, h_end, n_fee_side) ccd_nr = None lines_count = 0 @@ -456,7 +468,7 @@

    Module egse.dpu.hdf5_viewer

    ccd_nr = data_packet.header.type_as_object.ccd_number # N-FEE CCD number [0-3] # MODULE_LOGGER.info(f"before {ccd_nr=}") try: - ccd_nr = GlobalState.setup.camera.fee.ccd_numbering.CCD_BIN_TO_ID[ccd_nr] + ccd_nr = ccd_bin_to_id[ccd_nr] except AttributeError: raise SetupError("No entry in the setup for camera.fee.ccd_numbering.CCD_BIN_TO_ID") # MODULE_LOGGER.info(f"after {ccd_nr=}") @@ -531,7 +543,7 @@

    Module egse.dpu.hdf5_viewer

    Functions

    -def create_image(hdf_item: h5py._hl.group.Group, v_start: int, v_end: int, h_end: int, signals: WorkerSignals) +def create_image(hdf_item: h5py._hl.group.Group, v_start: int, v_end: int, h_end: int, n_fee_side, ccd_bin_to_id, signals: WorkerSignals)
    @@ -539,17 +551,15 @@

    Functions

    Expand source code -
    def create_image(hdf_item: h5py.Group, v_start: int, v_end: int, h_end: int,
    +
    def create_image(hdf_item: h5py.Group, v_start: int, v_end: int, h_end: int, n_fee_side, ccd_bin_to_id,
                      signals: WorkerSignals):
     
    -    n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum
    -
         # MODULE_LOGGER.info(f"Creating image: {v_start=}, {v_end=}")
     
         nr_lines = v_end - v_start + 1
         nr_columns = h_end + 1
     
    -    image = ImageCreatorFullSize(v_start, v_end, h_end)
    +    image = ImageCreatorFullSize(v_start, v_end, h_end, n_fee_side)
     
         ccd_nr = None
         lines_count = 0
    @@ -594,7 +604,7 @@ 

    Functions

    ccd_nr = data_packet.header.type_as_object.ccd_number # N-FEE CCD number [0-3] # MODULE_LOGGER.info(f"before {ccd_nr=}") try: - ccd_nr = GlobalState.setup.camera.fee.ccd_numbering.CCD_BIN_TO_ID[ccd_nr] + ccd_nr = ccd_bin_to_id[ccd_nr] except AttributeError: raise SetupError("No entry in the setup for camera.fee.ccd_numbering.CCD_BIN_TO_ID") # MODULE_LOGGER.info(f"after {ccd_nr=}") @@ -732,7 +742,7 @@

    Classes

    class MainWindow
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -741,7 +751,9 @@

    Classes

    def __init__(self): super().__init__() - self.n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum + setup = load_setup() + self.n_fee_side = setup.camera.fee.ccd_sides.enum + self.ccd_bin_to_id = setup.camera.fee.ccd_numbering.CCD_BIN_TO_ID # Setup the complete GUI @@ -819,6 +831,9 @@

    Classes

    self.data_view.setCurrentWidget(self.table_box) self.table_view.resizeColumnsToContents() self.proxy_model.layoutChanged.emit() + elif "hk_data" in hdf_item.name: + packet = HousekeepingData(hdf_item[()]) + self.display_hk_data_text(packet) elif "hk" in hdf_item.name: packet = HousekeepingPacket(hdf_item[()]) self.display_hk_packet_view(packet) @@ -837,6 +852,15 @@

    Classes

    if "data" in hdf_item: self.handle_image_data(hdf_item) + def display_hk_data_text(self, hk_data: HousekeepingData): + + console = Console(width=120, force_terminal=False, force_jupyter=False) + with console.capture() as capture: + console.print(hk_data) + text = capture.get() + self.text_box.setText(text) + self.data_view.setCurrentWidget(self.text_box) + def display_hk_packet_view(self, packet: HousekeepingPacket): hk_data = HousekeepingData(packet.data) @@ -900,7 +924,7 @@

    Classes

    # Create the Worker, any other args, kwargs are passed to the run function worker = Worker( - create_image, data_item, v_start, v_end, h_end + create_image, data_item, v_start, v_end, h_end, self.n_fee_side, self.ccd_bin_to_id, ) worker.signals.result.connect(self.show_image_data) worker.signals.finished.connect(self.worker_finished) @@ -957,6 +981,25 @@

    Ancestors

    Methods

    +
    +def display_hk_data_text(self, hk_data: HousekeepingData) +
    +
    +
    +
    + +Expand source code + +
    def display_hk_data_text(self, hk_data: HousekeepingData):
    +
    +    console = Console(width=120, force_terminal=False, force_jupyter=False)
    +    with console.capture() as capture:
    +        console.print(hk_data)
    +    text = capture.get()
    +    self.text_box.setText(text)
    +    self.data_view.setCurrentWidget(self.text_box)
    +
    +
    def display_hk_packet_text(self, packet: HousekeepingPacket)
    @@ -1049,7 +1092,7 @@

    Methods

    # Create the Worker, any other args, kwargs are passed to the run function worker = Worker( - create_image, data_item, v_start, v_end, h_end + create_image, data_item, v_start, v_end, h_end, self.n_fee_side, self.ccd_bin_to_id, ) worker.signals.result.connect(self.show_image_data) worker.signals.finished.connect(self.worker_finished) @@ -1141,6 +1184,9 @@

    Methods

    self.data_view.setCurrentWidget(self.table_box) self.table_view.resizeColumnsToContents() self.proxy_model.layoutChanged.emit() + elif "hk_data" in hdf_item.name: + packet = HousekeepingData(hdf_item[()]) + self.display_hk_data_text(packet) elif "hk" in hdf_item.name: packet = HousekeepingPacket(hdf_item[()]) self.display_hk_packet_view(packet) @@ -1427,6 +1473,7 @@

    Index

  • MainWindow

      +
    • display_hk_data_text
    • display_hk_packet_text
    • display_hk_packet_view
    • handle_image_data
    • diff --git a/docs/api/egse/dpu/hk_ui.html b/docs/api/egse/dpu/hk_ui.html index 8c01136..ae31ea8 100644 --- a/docs/api/egse/dpu/hk_ui.html +++ b/docs/api/egse/dpu/hk_ui.html @@ -510,7 +510,7 @@

      Classes

      (*args, **kwargs)
      -

      QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      +

      QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      Expand source code diff --git a/docs/api/egse/dpu/index.html b/docs/api/egse/dpu/index.html index eebe546..93154da 100644 --- a/docs/api/egse/dpu/index.html +++ b/docs/api/egse/dpu/index.html @@ -77,11 +77,10 @@

      Module egse.dpu

      import multiprocessing import pickle import queue -from enum import Enum - import time import traceback from dataclasses import dataclass +from enum import Enum from pathlib import Path from typing import Any from typing import Callable @@ -93,12 +92,14 @@

      Module egse.dpu

      import zmq +import egse.spw from egse.command import ClientServerCommand from egse.confman import ConfigurationManagerProxy from egse.control import ControlServer from egse.decorators import dynamic_interface -from egse.dpu.dpu import NFEEState, command_set_nfee_fpga_defaults +from egse.dpu.dpu import NFEEState from egse.dpu.dpu import command_external_clock +from egse.dpu.dpu import command_get_hk_information from egse.dpu.dpu import command_get_mode from egse.dpu.dpu import command_internal_clock from egse.dpu.dpu import command_reset @@ -111,7 +112,9 @@

      Module egse.dpu

      from egse.dpu.dpu import command_set_full_image_pattern_mode from egse.dpu.dpu import command_set_high_precision_hk_mode from egse.dpu.dpu import command_set_immediate_on_mode +from egse.dpu.dpu import command_set_nfee_fpga_defaults from egse.dpu.dpu import command_set_on_mode +from egse.dpu.dpu import command_set_readout_order from egse.dpu.dpu import command_set_register_value from egse.dpu.dpu import command_set_reverse_clocking from egse.dpu.dpu import command_set_standby_mode @@ -125,14 +128,15 @@

      Module egse.dpu

      from egse.dpu.dpu import prio_command_set_slicing from egse.dsi.esl import is_timecode from egse.exceptions import Abort -from egse.fee import is_hk_data_packet from egse.fee import n_fee_mode +from egse.fee.nfee import HousekeepingData from egse.obsid import ObservationIdentifier from egse.protocol import CommandProtocol from egse.proxy import Proxy from egse.reg import RegisterMap from egse.settings import Settings from egse.setup import SetupError +from egse.setup import load_setup from egse.spw import DataDataPacket from egse.spw import DataPacket from egse.spw import DataPacketType @@ -142,7 +146,6 @@

      Module egse.dpu

      from egse.spw import SpaceWirePacket from egse.spw import TimecodePacket from egse.spw import to_string -from egse.state import GlobalState from egse.storage import StorageProxy from egse.storage.persistence import FITS from egse.storage.persistence import HDF5 @@ -171,6 +174,19 @@

      Module egse.dpu

      "FITS": FITS, } +CCD_NUMBERS = [1, 2, 3, 4] + + +def rotate_list(seq, n): + if (size := len(seq)) < 2: + return seq + n = n % len(seq) + return seq[n:] + seq[:n] + + +def _get_ccd_readout_order(order_list: list, ccd_id_mapping: list): + return sum(ccd_id_mapping[ccd] << idx * 2 for idx, ccd in enumerate(order_list)) + class NoDataPacketError(Exception): """Raised when the expected data packet turns out to be something else.""" @@ -519,11 +535,15 @@

      Module egse.dpu

      self._command_q = command_queue self._response_q = response_queue + self._setup = load_setup() + if self._setup is None: + raise SetupError("The current Setup couldn't be loaded from the configuration manager.") + try: - self.default_ccd_readout_order = GlobalState.setup.camera.fee.ccd_numbering.DEFAULT_CCD_READOUT_ORDER - self.sensor_sel_both_sides = GlobalState.setup.camera.fee.sensor_sel.enum.BOTH_SIDES.value - except AttributeError: - raise SetupError("No entry in the setup for camera.fee.ccd_numbering.DEFAULT_CCD_READOUT_ORDER") + self.default_ccd_readout_order = self._setup.camera.fee.ccd_numbering.DEFAULT_CCD_READOUT_ORDER + self.sensor_sel_both_sides = self._setup.camera.fee.sensor_sel.enum.BOTH_SIDES.value + except AttributeError as exc: + raise SetupError("Missing entry in the setup for camera.fee group") from exc def marker(self, mark: str): LOGGER.info(f"{mark = }") @@ -614,10 +634,10 @@

      Module egse.dpu

      def n_fee_set_dump_mode(self, n_fee_parameters: dict): v_start = n_fee_parameters.get("v_start", 0) - v_end = n_fee_parameters.get("v_end", 4509) + v_end = n_fee_parameters.get("v_end", 0) sensor_sel_ = n_fee_parameters.get("sensor_sel", self.sensor_sel_both_sides) ccd_readout_order = n_fee_parameters.get("ccd_readout_order", self.default_ccd_readout_order) - n_final_dump = n_fee_parameters.get("n_final_dump", 0) + n_final_dump = n_fee_parameters.get("n_final_dump", 4510) sync_sel = n_fee_parameters.get("sync_sel", 0) num_cycles = n_fee_parameters.get("num_cycles", 0) self._command_q.put((command_set_dump_mode, @@ -632,12 +652,12 @@

      Module egse.dpu

      def n_fee_set_dump_mode_int_sync(self, n_fee_parameters: dict): v_start = n_fee_parameters.get("v_start", 0) - v_end = n_fee_parameters.get("v_end", 4509) + v_end = n_fee_parameters.get("v_end", 0) sensor_sel_ = n_fee_parameters.get("sensor_sel", self.sensor_sel_both_sides) - n_final_dump = n_fee_parameters.get("n_final_dump", 0) + n_final_dump = n_fee_parameters.get("n_final_dump", 4510) ccd_readout_order = n_fee_parameters.get("ccd_readout_order", self.default_ccd_readout_order) sync_sel = n_fee_parameters.get("sync_sel", 1) - int_sync_period = n_fee_parameters.get("int_sync_period", 2500) + int_sync_period = n_fee_parameters.get("int_sync_period", 600) num_cycles = n_fee_parameters.get("num_cycles", 0) self._command_q.put( ( @@ -839,9 +859,17 @@

      Module egse.dpu

      return response def n_fee_set_fpga_defaults(self): - self._command_q.put((command_set_nfee_fpga_defaults, [], {})) + """ + Loads the FPGA defaults from the Setup and commands the DPUProcessor to pass these defaults to the N-FEE. + + Returns: + The response from the DPU Processor after executing the command. + """ + fpga_defaults = self._setup.camera.fee.fpga_defaults + + self._command_q.put((command_set_nfee_fpga_defaults, [fpga_defaults], {})) - LOGGER.debug("Controller.n_fee_set_fpga_defaults: Put n_fee_set_fpga_defaults on the Queue.") + LOGGER.debug("Controller.n_fee_set_fpga_defaults: Put command_set_nfee_fpga_defaults on the Queue.") (cmd, response) = self._response_q.get() @@ -1388,6 +1416,9 @@

      Module egse.dpu

      # Enumeration with the sensor_sel sensor_sel_enum: Enum = None + # Mapping of the CCD identifier to the binary representation (loaded from the Setup) + ccd_id_to_bin: List[int] = None + # The clear_error_flags shall be executed on every readout, i.e. every 200ms and 400ms pulse. clear_error_flags = False @@ -1395,6 +1426,34 @@

      Module egse.dpu

      # saved in the HDF5 file upon reception. slicing_num_cycles = 0 + # When in internal sync the ccd_readout_order is not used like in external sync mode. + # Each readout is done on the same CCD, i.e. the first CCD number in the ccd_readout_order list. + # Therefore, we will rotate this list on each readout in internal sync dump mode to guarantee + # each CCD is cleared out. + + # Initialise cycling of CCDs in internal sync dump mode to the default CCD numbering. + current_ccd_readout_order = CCD_NUMBERS + + # The cycle_count goes from [0 -> 3] to make sure that, during internal sync dump mode, we have cleared out + # all four CCDs. A clear-out cycle can only be interrupted when cycle_count == 0, at that time all commands + # on the queue will be executed. + cycle_count = 0 + + def reset_int_sync_dump_mode(self, ccd_numbers: list = None): + """ + Resets the cycle_count to zero (0) and the current ccd_readout_order to the given ccd_numbers. + When ccd_numbers is None the default CCD readout order will be used, i.e. CCD_NUMBERS, [1,2,3,4]. + + Args: + ccd_numbers: a list of four CCD numbers going from 1 to 4. + + """ + self.current_ccd_readout_order = ccd_numbers or CCD_NUMBERS + self.cycle_count = 0 + + def int_sync_cycle_dump_mode(self): + """Returns True if we are in internal sync dump mode.""" + return self.internal_sync and self.dump_mode and self.num_cycles < 0 def is_start_of_cycle(self): """ @@ -1408,10 +1467,7 @@

      Module egse.dpu

      Returns True if in the last readout in this cycle. Note that, when in internal sync mode, this method always returns True. """ - if self.internal_sync: - return True - else: - return self.frame_number == 3 + return True if self.internal_sync else self.frame_number == 3 def is_400ms_pulse(self): return self.frame_number == 0 @@ -1420,7 +1476,7 @@

      Module egse.dpu

      return self.frame_number in [1, 2, 3] def update(self, n_fee_state: NFEEState.StateTuple): - self.dump_mode = not bool(n_fee_state.digitise_en) + self.dump_mode = n_fee_state.ccd_mode_config == n_fee_mode.FULL_IMAGE_MODE and not bool(n_fee_state.digitise_en) self.internal_sync = bool(n_fee_state.sync_sel) self.expected_last_packet_flags = create_expected_last_packet_flags(n_fee_state, self.sensor_sel_enum) @@ -1457,21 +1513,30 @@

      Module egse.dpu

      self.register_map = RegisterMap("N-FEE") self._quit_event = multiprocessing.Event() - # These will be properly initialized when the register map is read from the N-FEE + # The following variables will be initialised in the run() method. - self._n_fee_state = NFEEState() + self._setup = None + self._dpu_internals = None + + # These will be properly initialized when the register map is read from the N-FEE. + self._n_fee_state = NFEEState() def run(self): + self._setup = load_setup() + if self._setup is None: + raise SetupError("Couldn't load the current Setup from the configuration manager.") + self._dpu_internals = DPUInternals( num_cycles=-1, expected_last_packet_flags=[False, False, False, False], dump_mode=False, internal_sync=False, frame_number=-1, - ccd_sides_enum=GlobalState.setup.camera.fee.ccd_sides.enum, - sensor_sel_enum=GlobalState.setup.camera.fee.sensor_sel.enum, + ccd_sides_enum=self._setup.camera.fee.ccd_sides.enum, + sensor_sel_enum=self._setup.camera.fee.sensor_sel.enum, + ccd_id_to_bin=self._setup.camera.fee.ccd_numbering.CCD_ID_TO_BIN, ) # The DPU Processor runs in a different process and since ZeroMQ Sockets are not @@ -1640,6 +1705,7 @@

      Module egse.dpu

      if self._dpu_internals.num_cycles == 0: if self._dpu_internals.dump_mode_int: + self._dpu_internals.reset_int_sync_dump_mode() dump_mode_command = command_set_dump_mode_int_sync else: dump_mode_command = command_set_dump_mode @@ -1657,6 +1723,46 @@

      Module egse.dpu

      self._transport, storage, origin_spw_data, start_time, mode, self.register_map, data_attr, self._dpu_internals, dist_sock) + # Read HK packet from N-FEE memory map + # see #2478 [https://github.com/IvS-KULeuven/plato-common-egse/issues/2478] + + time.sleep(0.012) # add 12ms to make sure HK data has been updated on the N-FEE + + with Timer("Read and process updated HK data"): + hk_data, timestamp = read_updated_hk_data(self._transport) + process_updated_hk_data(hk_data, timestamp, storage, origin_spw_data, + self._dpu_internals.frame_number, mon_sock, dist_sock) + + if self._dpu_internals.int_sync_cycle_dump_mode(): + LOGGER.warning("Cycling CCD readout in internal sync") + + # When we are in internal sync and dump mode, we need to cycle through the four CCDs, and + # we need an atomic block of four clear outs. + + internals = self._dpu_internals + internals.current_ccd_readout_order = rotate_list(internals.current_ccd_readout_order, 1) + internals.cycle_count += 1 + ccd_readout_order = _get_ccd_readout_order( + internals.current_ccd_readout_order, internals.ccd_id_to_bin + ) + # LOGGER.info(f"{internals.current_ccd_readout_order = }, {ccd_readout_order = }, " + # f"{internals.cycle_count = }") + _ = command_set_readout_order(self._transport, self.register_map, ccd_readout_order) + + # We agreed to have atomic blocks of 4 clear-outs such that all four CCDs would always + # be dumped. So, whenever we are within one such atomic block, don't execute any DPU + # commands. + + if internals.cycle_count < 4: + LOGGER.debug( + f"[1] {internals.current_ccd_readout_order = }, {ccd_readout_order = }, " + f"{internals.cycle_count = }") + else: + internals.cycle_count = 0 + LOGGER.debug( + f"[2] {internals.current_ccd_readout_order = }, {ccd_readout_order = }, " + f"{internals.cycle_count = }") + except NoBytesReceivedError as exc: # LOGGER.debug(f"No bytes received: {exc}") pass @@ -1698,6 +1804,13 @@

      Module egse.dpu

      self._priority_q, self._response_q, self._n_fee_state.get_state(), self._dpu_internals, self.register_map) + # When we are in internal sync dump mode, we need atomic blocks of 4 readouts such that all four + # CCDs will be cleared out. The cycle count goes from [0 -> 3] so, we only send commands when + # cycle count == 0. + # LOGGER.debug(f"{self._dpu_internals.cycle_count = }") + if self._dpu_internals.int_sync_cycle_dump_mode() and self._dpu_internals.cycle_count != 0: + continue + # Then, we might want to send some RMAP commands ------------------------------- # When we are in the 2s RMAP window, send the commands. @@ -1779,7 +1892,7 @@

      Module egse.dpu

      LOGGER.debug(f"Timecode received {packet=}") - while time.perf_counter() < start_time + 4.2: + while time.perf_counter() < start_time + 4.2: # FIXME: assuming N-FEE in external sync when starting... terminator, packet = self._transport.read_packet(timeout=200) if packet is None: msg = f"time passed {time.perf_counter() - start_time:0.3f}" @@ -1905,9 +2018,10 @@

      Module egse.dpu

      # 2.2 - Multiple commands can now be saved under the same frame number # 2.3 - introduced /dpu/num_cycles attribute # 2.4 - introduced /dpu/slicing_num_cycles attribute + # 2.5 - introduced /{frame number}/hk_data dataset major_version = 2 - minor_version = 4 + minor_version = 5 item_data = { "/versions/format_version/": "format version of HDF5 file", @@ -1993,18 +2107,13 @@

      Module egse.dpu

      def read_timecode(transport: SpaceWireInterface) -> (TimecodePacket, str, float): """ - Reads the next Timecode packet from the N-FEE. When the Timecode packet is received, - the packet and a timestamp is sent to the Storage manager. + Reads the next Timecode packet from the N-FEE. Args: transport: the SpaceWire interfaces that is used for communication to the N-FEE - storage: the proxy that is used to communicate with the Storage manager - origin_spw_data: the registration identifier for the Storage manager, for the SpW data - mon_socket: the ZeroMQ socket to which monitoring sync signals are sent - dist_socket: the ZeroMQ socket to which SpW data is sent (for real-time view) Returns: - The approximate start time for this readout cycle. + The timecode and associated timestamp, and the approximate start time for this readout cycle. Raises: NoTimecodeError when the timecode could not be read. """ @@ -2035,7 +2144,7 @@

      Module egse.dpu

      tc_packet: TimecodePacket = SpaceWirePacket.create_packet(packet) - LOGGER.info(f"Timecode received: {tc_packet.timecode}") + LOGGER.info(f"Timecode received: 0x{tc_packet.timecode:0X} ({tc_packet.timecode})") return tc_packet, timestamp, start_time @@ -2043,7 +2152,22 @@

      Module egse.dpu

      def process_timecode(tc_packet: TimecodePacket, timestamp: str, storage: StorageProxy, origin_spw_data: str, frame_number: int, mon_socket: zmq.Socket, dist_socket: zmq.Socket): + """ + Saves the timecode and associated timestamp for this frame. The timecode and timestamp + are also published on the monitoring and data distribution message queue. + + Args: + tc_packet: the timecode packet + timestamp: a timestamp associated with the reception of the timecode + frame_number: the current frame number + storage: the proxy that is used to communicate with the Storage manager + origin_spw_data: the registration identifier for the Storage manager, for the SpW data + mon_socket: the ZeroMQ socket to which monitoring sync signals are sent + dist_socket: the ZeroMQ socket to which SpW data is sent (for real-time view) + Returns: + Nothing. + """ LOGGER.debug(f"Saving timecode packet: {tc_packet.timecode=}, {frame_number=}") response = storage.save( @@ -2064,40 +2188,121 @@

      Module egse.dpu

      dist_socket.send_multipart([MessageIdentifier.SYNC_TIMECODE.to_bytes(1, "big"), pickle_string]) -def read_hk_packet(transport: SpaceWireInterface) -> (HousekeepingPacket, str): +def read_updated_hk_data(transport: SpaceWireInterface) -> (HousekeepingData, str): """ - Read the next Housekeeping Packet from the N-FEE. The packet is sent to the Storage manager. + Reads the memory map that contains the housekeeping information from the N-FEE. + The memory map is returned as a HousekeepingData object. + + This is not the same as a housekeeping packet that is read from the N-FEE. For + that refer to the function `read_hk_packet()`. Args: transport: the SpaceWire interfaces that is used for communication to the N-FEE + + Returns: + The HK data packet and its associated timestamp as a string. + """ + timestamp = format_datetime() + + data = command_get_hk_information(transport, None, 0x000_0700, 0x90) + hk_data = HousekeepingData(data) + + msg = f"Updated housekeeping retrieved... {hk_data.frame_counter = }, {hk_data.timecode = }" + if hk_data.error_flags: + msg += f", error_flags = 0b{hk_data.error_flags:032b}" + LOGGER.warning(msg) + else: + LOGGER.info(msg) + + return hk_data, timestamp + +def process_updated_hk_data(hk_data: HousekeepingData, timestamp: str, storage: StorageProxy, + origin: str, frame_number: int, + mon_socket: zmq.Socket, dist_socket: zmq.Socket): + """ + Saves the housekeeping data and associated timestamp for this frame. The data and timestamp + are also published on the monitoring message queue. + + Args: + hk_data: the HousekeepingData object + timestamp: the timestamp associated with the reception of this data + frame_number: the current frame number storage: the proxy that is used to communicate with the Storage manager origin: the registration identifier for the Storage manager mon_socket: the ZeroMQ socket to which monitoring sync signals are sent dist_socket: the ZeroMQ socket to which SpW data is sent (for real-time view) + Returns: + Nothing. + """ + LOGGER.debug(f"Saving updated Housekeeping data: {hk_data.frame_counter = }, {hk_data.timecode = }, {hk_data.frame_number = }") + + response = storage.save( + { + "origin": origin, + "data": + { + f"/{frame_number}/hk_data": hk_data, + f"/{frame_number}/hk_data:ATTRS": [("timestamp", timestamp)], + } + } + ) + + LOGGER.debug(f"Response from saving updated Housekeeping data: {response}") + + pickle_string = pickle.dumps((hk_data.error_flags, hk_data.frame_counter, timestamp)) + mon_socket.send_multipart([MessageIdentifier.SYNC_ERROR_FLAGS.to_bytes(1, "big"), pickle_string]) + + msg_id = MessageIdentifier.SYNC_HK_DATA.to_bytes(1, 'big') + + pickle_string = pickle.dumps((hk_data, timestamp)) + dist_socket.send_multipart([msg_id, pickle_string]) + + +def read_hk_packet(transport: SpaceWireInterface) -> (HousekeepingPacket, str): + """ + Read the next Housekeeping Packet from the N-FEE. + + Args: + transport: the SpaceWire interfaces that is used for communication to the N-FEE Raises: NoHousekeepingPacketError when the next packet is not a `HousekeepingPacket`. Returns: - the received housekeeping packet. + the received housekeeping packet and the timestamp. """ terminator, packet = transport.read_packet() timestamp = format_datetime() - if not is_hk_data_packet(packet): - packet = SpaceWirePacket.create_packet(packet) - raise NoHousekeepingPacketError( - f"Expected a Housekeeping packet, but got {packet.__class__.__name__}") + packet = SpaceWirePacket.create_packet(packet) - hk_packet: HousekeepingPacket = SpaceWirePacket.create_packet(packet) + if not isinstance(packet, egse.spw.HousekeepingPacket): + raise NoHousekeepingPacketError( + f"Expected a HousekeepingPacket, but got {packet.__class__.__module__}.{packet.__class__.__name__}") - LOGGER.info(f"Housekeeping Packet received: {hk_packet.type!s}") + LOGGER.info(f"Housekeeping Packet received: {packet.type!s}") - return hk_packet, timestamp + return packet, timestamp def process_hk_packet(hk_packet: HousekeepingPacket, timestamp: str, storage: StorageProxy, origin: str, frame_number: int, mon_socket: zmq.Socket, dist_socket: zmq.Socket): + """ + Saves the housekeeping packet and associated timestamp for this frame. The data and timestamp + are also published on the monitoring and data distribution message queue. + + Args: + hk_packet: the HousekeepingPacket + timestamp: the timestamp associated with the reception of this packet + frame_number: the current frame number + storage: the proxy that is used to communicate with the Storage manager + origin: the registration identifier for the Storage manager + mon_socket: the ZeroMQ socket to which monitoring sync signals are sent + dist_socket: the ZeroMQ socket to which SpW data is sent (for real-time view) + + Returns: + Nothing. + """ LOGGER.debug(f"Saving Housekeeping packet: {hk_packet.type!s}, " f"frame counter={hk_packet.frame_counter}, " @@ -2167,7 +2372,7 @@

      Module egse.dpu

      # Read the data, until all the expected last packet bits are set. # This should be within next 4 seconds. - LOGGER.debug("Reading data packets....") + LOGGER.info("Reading data packets....") terminator, packet = transport.read_packet() @@ -2296,8 +2501,12 @@

      Module egse.dpu

      """ if internals.clear_error_flags: - LOGGER.debug("Clearing error flags") - _ = command_set_clear_error_flags(transport, register_map) + LOGGER.debug("Set the clear-error-flags register parameter.") + try: + _ = command_set_clear_error_flags(transport, register_map) + except ValueError as exc: + LOGGER.error("The clear-error-flags register parameter could not be set due to a ValueError.", exc_info=exc) + internals.clear_error_flags = False command = response = None @@ -2560,10 +2769,6 @@

      Sub-modules

      This module define the FITS generation process …

      -
      egse.dpu.fitsgen_test
      -
      -
      -
      egse.dpu.fitsgen_ui
      @@ -2898,7 +3103,7 @@

      Args

      -def process_high_priority_commands(priority_q: >, response_q: >, n_fee_state: tuple, dpu_internals: DPUInternals, reg_map: RegisterMap) +def process_high_priority_commands(priority_q: >, response_q: >, n_fee_state: tuple, dpu_internals: DPUInternals, reg_map: RegisterMap)

      Execute high priority commands from the DPU Control Server / @@ -2965,7 +3170,27 @@

      Args

      def process_hk_packet(hk_packet: HousekeepingPacket, timestamp: str, storage: StorageProxy, origin: str, frame_number: int, mon_socket: zmq.sugar.socket.Socket, dist_socket: zmq.sugar.socket.Socket)
      -
      +

      Saves the housekeeping packet and associated timestamp for this frame. The data and timestamp +are also published on the monitoring and data distribution message queue.

      +

      Args

      +
      +
      hk_packet
      +
      the HousekeepingPacket
      +
      timestamp
      +
      the timestamp associated with the reception of this packet
      +
      frame_number
      +
      the current frame number
      +
      storage
      +
      the proxy that is used to communicate with the Storage manager
      +
      origin
      +
      the registration identifier for the Storage manager
      +
      mon_socket
      +
      the ZeroMQ socket to which monitoring sync signals are sent
      +
      dist_socket
      +
      the ZeroMQ socket to which SpW data is sent (for real-time view)
      +
      +

      Returns

      +

      Nothing.

      Expand source code @@ -2973,6 +3198,22 @@

      Args

      def process_hk_packet(hk_packet: HousekeepingPacket, timestamp: str,
                             storage: StorageProxy, origin: str, frame_number: int,
                             mon_socket: zmq.Socket, dist_socket: zmq.Socket):
      +    """
      +    Saves the housekeeping packet and associated timestamp for this frame. The data and timestamp
      +    are also published on the monitoring and data distribution message queue.
      +
      +    Args:
      +        hk_packet: the HousekeepingPacket
      +        timestamp: the timestamp associated with the reception of this packet
      +        frame_number: the current frame number
      +        storage: the proxy that is used to communicate with the Storage manager
      +        origin: the registration identifier for the Storage manager
      +        mon_socket: the ZeroMQ socket to which monitoring sync signals are sent
      +        dist_socket: the ZeroMQ socket to which SpW data is sent (for real-time view)
      +
      +    Returns:
      +        Nothing.
      +    """
       
           LOGGER.debug(f"Saving Housekeeping packet: {hk_packet.type!s}, "
                       f"frame counter={hk_packet.frame_counter}, "
      @@ -3003,7 +3244,27 @@ 

      Args

      def process_timecode(tc_packet: TimecodePacket, timestamp: str, storage: StorageProxy, origin_spw_data: str, frame_number: int, mon_socket: zmq.sugar.socket.Socket, dist_socket: zmq.sugar.socket.Socket)
      -
      +

      Saves the timecode and associated timestamp for this frame. The timecode and timestamp +are also published on the monitoring and data distribution message queue.

      +

      Args

      +
      +
      tc_packet
      +
      the timecode packet
      +
      timestamp
      +
      a timestamp associated with the reception of the timecode
      +
      frame_number
      +
      the current frame number
      +
      storage
      +
      the proxy that is used to communicate with the Storage manager
      +
      origin_spw_data
      +
      the registration identifier for the Storage manager, for the SpW data
      +
      mon_socket
      +
      the ZeroMQ socket to which monitoring sync signals are sent
      +
      dist_socket
      +
      the ZeroMQ socket to which SpW data is sent (for real-time view)
      +
      +

      Returns

      +

      Nothing.

      Expand source code @@ -3011,7 +3272,22 @@

      Args

      def process_timecode(tc_packet: TimecodePacket, timestamp: str,
                            storage: StorageProxy, origin_spw_data: str, frame_number: int,
                            mon_socket: zmq.Socket, dist_socket: zmq.Socket):
      +    """
      +    Saves the timecode and associated timestamp for this frame. The timecode and timestamp
      +    are also published on the monitoring and data distribution message queue.
       
      +    Args:
      +        tc_packet: the timecode packet
      +        timestamp: a timestamp associated with the reception of the timecode
      +        frame_number: the current frame number
      +        storage: the proxy that is used to communicate with the Storage manager
      +        origin_spw_data: the registration identifier for the Storage manager, for the SpW data
      +        mon_socket: the ZeroMQ socket to which monitoring sync signals are sent
      +        dist_socket: the ZeroMQ socket to which SpW data is sent (for real-time view)
      +
      +    Returns:
      +        Nothing.
      +    """
           LOGGER.debug(f"Saving timecode packet: {tc_packet.timecode=}, {frame_number=}")
       
           response = storage.save(
      @@ -3032,6 +3308,78 @@ 

      Args

      dist_socket.send_multipart([MessageIdentifier.SYNC_TIMECODE.to_bytes(1, "big"), pickle_string])
      +
      +def process_updated_hk_data(hk_data: HousekeepingData, timestamp: str, storage: StorageProxy, origin: str, frame_number: int, mon_socket: zmq.sugar.socket.Socket, dist_socket: zmq.sugar.socket.Socket) +
      +
      +

      Saves the housekeeping data and associated timestamp for this frame. The data and timestamp +are also published on the monitoring message queue.

      +

      Args

      +
      +
      hk_data
      +
      the HousekeepingData object
      +
      timestamp
      +
      the timestamp associated with the reception of this data
      +
      frame_number
      +
      the current frame number
      +
      storage
      +
      the proxy that is used to communicate with the Storage manager
      +
      origin
      +
      the registration identifier for the Storage manager
      +
      mon_socket
      +
      the ZeroMQ socket to which monitoring sync signals are sent
      +
      dist_socket
      +
      the ZeroMQ socket to which SpW data is sent (for real-time view)
      +
      +

      Returns

      +

      Nothing.

      +
      + +Expand source code + +
      def process_updated_hk_data(hk_data: HousekeepingData, timestamp: str, storage: StorageProxy,
      +                            origin: str, frame_number: int,
      +                            mon_socket: zmq.Socket, dist_socket: zmq.Socket):
      +    """
      +    Saves the housekeeping data and associated timestamp for this frame. The data and timestamp
      +    are also published on the monitoring message queue.
      +
      +    Args:
      +        hk_data: the HousekeepingData object
      +        timestamp: the timestamp associated with the reception of this data
      +        frame_number: the current frame number
      +        storage: the proxy that is used to communicate with the Storage manager
      +        origin: the registration identifier for the Storage manager
      +        mon_socket: the ZeroMQ socket to which monitoring sync signals are sent
      +        dist_socket: the ZeroMQ socket to which SpW data is sent (for real-time view)
      +
      +    Returns:
      +        Nothing.
      +    """
      +    LOGGER.debug(f"Saving updated Housekeeping data: {hk_data.frame_counter = }, {hk_data.timecode = }, {hk_data.frame_number = }")
      +
      +    response = storage.save(
      +        {
      +            "origin": origin,
      +            "data":
      +                {
      +                    f"/{frame_number}/hk_data": hk_data,
      +                    f"/{frame_number}/hk_data:ATTRS": [("timestamp", timestamp)],
      +                }
      +        }
      +    )
      +
      +    LOGGER.debug(f"Response from saving updated Housekeeping data: {response}")
      +
      +    pickle_string = pickle.dumps((hk_data.error_flags, hk_data.frame_counter, timestamp))
      +    mon_socket.send_multipart([MessageIdentifier.SYNC_ERROR_FLAGS.to_bytes(1, "big"), pickle_string])
      +
      +    msg_id = MessageIdentifier.SYNC_HK_DATA.to_bytes(1, 'big')
      +
      +    pickle_string = pickle.dumps((hk_data, timestamp))
      +    dist_socket.send_multipart([msg_id, pickle_string])
      +
      +
      def read_and_process_data_packets(transport: SpaceWireInterface, storage: StorageProxy, origin_spw_data: str, start_time: float, mode: int, register_map: RegisterMap, data_attr: dict, internals: DPUInternals, dist_socket: zmq.sugar.socket.Socket)
      @@ -3107,7 +3455,7 @@

      Raises

      # Read the data, until all the expected last packet bits are set. # This should be within next 4 seconds. - LOGGER.debug("Reading data packets....") + LOGGER.info("Reading data packets....") terminator, packet = transport.read_packet() @@ -3211,80 +3559,57 @@

      Raises

      def read_hk_packet(transport: SpaceWireInterface) ‑> (HousekeepingPacket'>, )
      -

      Read the next Housekeeping Packet from the N-FEE. The packet is sent to the Storage manager.

      +

      Read the next Housekeeping Packet from the N-FEE.

      Args

      transport
      the SpaceWire interfaces that is used for communication to the N-FEE
      -
      storage
      -
      the proxy that is used to communicate with the Storage manager
      -
      origin
      -
      the registration identifier for the Storage manager
      -
      mon_socket
      -
      the ZeroMQ socket to which monitoring sync signals are sent
      -
      dist_socket
      -
      the ZeroMQ socket to which SpW data is sent (for real-time view)

      Raises

      NoHousekeepingPacketError when the next packet is not a HousekeepingPacket.

      Returns

      -

      the received housekeeping packet.

      +

      the received housekeeping packet and the timestamp.

      Expand source code
      def read_hk_packet(transport: SpaceWireInterface) -> (HousekeepingPacket, str):
           """
      -    Read the next Housekeeping Packet from the N-FEE. The packet is sent to the Storage manager.
      +    Read the next Housekeeping Packet from the N-FEE.
       
           Args:
               transport: the SpaceWire interfaces that is used for communication to the N-FEE
      -        storage: the proxy that is used to communicate with the Storage manager
      -        origin: the registration identifier for the Storage manager
      -        mon_socket: the ZeroMQ socket to which monitoring sync signals are sent
      -        dist_socket: the ZeroMQ socket to which SpW data is sent (for real-time view)
      -
           Raises:
               NoHousekeepingPacketError when the next packet is not a `HousekeepingPacket`.
           Returns:
      -        the received housekeeping packet.
      +        the received housekeeping packet and the timestamp.
           """
           terminator, packet = transport.read_packet()
           timestamp = format_datetime()
       
      -    if not is_hk_data_packet(packet):
      -        packet = SpaceWirePacket.create_packet(packet)
      -        raise NoHousekeepingPacketError(
      -            f"Expected a Housekeeping packet, but got {packet.__class__.__name__}")
      +    packet = SpaceWirePacket.create_packet(packet)
       
      -    hk_packet: HousekeepingPacket = SpaceWirePacket.create_packet(packet)
      +    if not isinstance(packet, egse.spw.HousekeepingPacket):
      +        raise NoHousekeepingPacketError(
      +            f"Expected a HousekeepingPacket, but got {packet.__class__.__module__}.{packet.__class__.__name__}")
       
      -    LOGGER.info(f"Housekeeping Packet received: {hk_packet.type!s}")
      +    LOGGER.info(f"Housekeeping Packet received: {packet.type!s}")
       
      -    return hk_packet, timestamp
      + return packet, timestamp
      def read_timecode(transport: SpaceWireInterface) ‑> (TimecodePacket'>, )
      -

      Reads the next Timecode packet from the N-FEE. When the Timecode packet is received, -the packet and a timestamp is sent to the Storage manager.

      +

      Reads the next Timecode packet from the N-FEE.

      Args

      transport
      the SpaceWire interfaces that is used for communication to the N-FEE
      -
      storage
      -
      the proxy that is used to communicate with the Storage manager
      -
      origin_spw_data
      -
      the registration identifier for the Storage manager, for the SpW data
      -
      mon_socket
      -
      the ZeroMQ socket to which monitoring sync signals are sent
      -
      dist_socket
      -
      the ZeroMQ socket to which SpW data is sent (for real-time view)

      Returns

      -

      The approximate start time for this readout cycle.

      +

      The timecode and associated timestamp, and the approximate start time for this readout cycle.

      Raises

      NoTimecodeError when the timecode could not be read.

      @@ -3293,18 +3618,13 @@

      Raises

  • def read_timecode(transport: SpaceWireInterface) -> (TimecodePacket, str, float):
         """
    -    Reads the next Timecode packet from the N-FEE. When the Timecode packet is received,
    -    the packet and a timestamp is sent to the Storage manager.
    +    Reads the next Timecode packet from the N-FEE.
     
         Args:
             transport: the SpaceWire interfaces that is used for communication to the N-FEE
    -        storage: the proxy that is used to communicate with the Storage manager
    -        origin_spw_data: the registration identifier for the Storage manager, for the SpW data
    -        mon_socket: the ZeroMQ socket to which monitoring sync signals are sent
    -        dist_socket: the ZeroMQ socket to which SpW data is sent (for real-time view)
     
         Returns:
    -        The approximate start time for this readout cycle.
    +        The timecode and associated timestamp, and the approximate start time for this readout cycle.
         Raises:
             NoTimecodeError when the timecode could not be read.
         """
    @@ -3335,11 +3655,59 @@ 

    Raises

    tc_packet: TimecodePacket = SpaceWirePacket.create_packet(packet) - LOGGER.info(f"Timecode received: {tc_packet.timecode}") + LOGGER.info(f"Timecode received: 0x{tc_packet.timecode:0X} ({tc_packet.timecode})") return tc_packet, timestamp, start_time
    +
    +def read_updated_hk_data(transport: SpaceWireInterface) ‑> (HousekeepingData'>, ) +
    +
    +

    Reads the memory map that contains the housekeeping information from the N-FEE. +The memory map is returned as a HousekeepingData object.

    +

    This is not the same as a housekeeping packet that is read from the N-FEE. For +that refer to the function read_hk_packet().

    +

    Args

    +
    +
    transport
    +
    the SpaceWire interfaces that is used for communication to the N-FEE
    +
    +

    Returns

    +

    The HK data packet and its associated timestamp as a string.

    +
    + +Expand source code + +
    def read_updated_hk_data(transport: SpaceWireInterface) -> (HousekeepingData, str):
    +    """
    +    Reads the memory map that contains the housekeeping information from the N-FEE.
    +    The memory map is returned as a HousekeepingData object.
    +
    +    This is not the same as a housekeeping packet that is read from the N-FEE. For
    +    that refer to the function `read_hk_packet()`.
    +
    +    Args:
    +        transport: the SpaceWire interfaces that is used for communication to the N-FEE
    +
    +    Returns:
    +        The HK data packet and its associated timestamp as a string.
    +    """
    +    timestamp = format_datetime()
    +
    +    data = command_get_hk_information(transport, None, 0x000_0700, 0x90)
    +    hk_data = HousekeepingData(data)
    +
    +    msg = f"Updated housekeeping retrieved... {hk_data.frame_counter = }, {hk_data.timecode = }"
    +    if hk_data.error_flags:
    +        msg += f", error_flags = 0b{hk_data.error_flags:032b}"
    +        LOGGER.warning(msg)
    +    else:
    +        LOGGER.info(msg)
    +
    +    return hk_data, timestamp
    +
    +
    def register_to_storage_manager(proxy: StorageProxy, origin: str)
    @@ -3365,6 +3733,22 @@

    Raises

    LOGGER.warning(f"Couldn't register to the Storage manager: {rc}")
    +
    +def rotate_list(seq, n) +
    +
    +
    +
    + +Expand source code + +
    def rotate_list(seq, n):
    +    if (size := len(seq)) < 2:
    +        return seq
    +    n = n % len(seq)
    +    return seq[n:] + seq[:n]
    +
    +
    def save_format_version(proxy: StorageProxy, origin: str)
    @@ -3381,9 +3765,10 @@

    Raises

    # 2.2 - Multiple commands can now be saved under the same frame number # 2.3 - introduced /dpu/num_cycles attribute # 2.4 - introduced /dpu/slicing_num_cycles attribute + # 2.5 - introduced /{frame number}/hk_data dataset major_version = 2 - minor_version = 4 + minor_version = 5 item_data = { "/versions/format_version/": "format version of HDF5 file", @@ -3511,7 +3896,7 @@

    Raises

    -def send_commands_to_n_fee(transport: SpaceWireInterface, storage: StorageProxy, origin: str, register_map: RegisterMap, command_q: >, response_q: >, internals: DPUInternals) +def send_commands_to_n_fee(transport: SpaceWireInterface, storage: StorageProxy, origin: str, register_map: RegisterMap, command_q: >, response_q: >, internals: DPUInternals)

    Send RMAP commands to the N-FEE. The commands are read from the command queue that is shared @@ -3573,8 +3958,12 @@

    Raises

    """ if internals.clear_error_flags: - LOGGER.debug("Clearing error flags") - _ = command_set_clear_error_flags(transport, register_map) + LOGGER.debug("Set the clear-error-flags register parameter.") + try: + _ = command_set_clear_error_flags(transport, register_map) + except ValueError as exc: + LOGGER.error("The clear-error-flags register parameter could not be set due to a ValueError.", exc_info=exc) + internals.clear_error_flags = False command = response = None @@ -3719,7 +4108,7 @@

    Inherited members

    class DPUController -(priority_queue: >, command_queue: >, response_queue: >) +(priority_queue: >, command_queue: >, response_queue: >)

    The DPU Controller puts commands on the command queue for processing by the DPU Processor. @@ -3746,11 +4135,15 @@

    Inherited members

    self._command_q = command_queue self._response_q = response_queue + self._setup = load_setup() + if self._setup is None: + raise SetupError("The current Setup couldn't be loaded from the configuration manager.") + try: - self.default_ccd_readout_order = GlobalState.setup.camera.fee.ccd_numbering.DEFAULT_CCD_READOUT_ORDER - self.sensor_sel_both_sides = GlobalState.setup.camera.fee.sensor_sel.enum.BOTH_SIDES.value - except AttributeError: - raise SetupError("No entry in the setup for camera.fee.ccd_numbering.DEFAULT_CCD_READOUT_ORDER") + self.default_ccd_readout_order = self._setup.camera.fee.ccd_numbering.DEFAULT_CCD_READOUT_ORDER + self.sensor_sel_both_sides = self._setup.camera.fee.sensor_sel.enum.BOTH_SIDES.value + except AttributeError as exc: + raise SetupError("Missing entry in the setup for camera.fee group") from exc def marker(self, mark: str): LOGGER.info(f"{mark = }") @@ -3841,10 +4234,10 @@

    Inherited members

    def n_fee_set_dump_mode(self, n_fee_parameters: dict): v_start = n_fee_parameters.get("v_start", 0) - v_end = n_fee_parameters.get("v_end", 4509) + v_end = n_fee_parameters.get("v_end", 0) sensor_sel_ = n_fee_parameters.get("sensor_sel", self.sensor_sel_both_sides) ccd_readout_order = n_fee_parameters.get("ccd_readout_order", self.default_ccd_readout_order) - n_final_dump = n_fee_parameters.get("n_final_dump", 0) + n_final_dump = n_fee_parameters.get("n_final_dump", 4510) sync_sel = n_fee_parameters.get("sync_sel", 0) num_cycles = n_fee_parameters.get("num_cycles", 0) self._command_q.put((command_set_dump_mode, @@ -3859,12 +4252,12 @@

    Inherited members

    def n_fee_set_dump_mode_int_sync(self, n_fee_parameters: dict): v_start = n_fee_parameters.get("v_start", 0) - v_end = n_fee_parameters.get("v_end", 4509) + v_end = n_fee_parameters.get("v_end", 0) sensor_sel_ = n_fee_parameters.get("sensor_sel", self.sensor_sel_both_sides) - n_final_dump = n_fee_parameters.get("n_final_dump", 0) + n_final_dump = n_fee_parameters.get("n_final_dump", 4510) ccd_readout_order = n_fee_parameters.get("ccd_readout_order", self.default_ccd_readout_order) sync_sel = n_fee_parameters.get("sync_sel", 1) - int_sync_period = n_fee_parameters.get("int_sync_period", 2500) + int_sync_period = n_fee_parameters.get("int_sync_period", 600) num_cycles = n_fee_parameters.get("num_cycles", 0) self._command_q.put( ( @@ -4066,9 +4459,17 @@

    Inherited members

    return response def n_fee_set_fpga_defaults(self): - self._command_q.put((command_set_nfee_fpga_defaults, [], {})) + """ + Loads the FPGA defaults from the Setup and commands the DPUProcessor to pass these defaults to the N-FEE. + + Returns: + The response from the DPU Processor after executing the command. + """ + fpga_defaults = self._setup.camera.fee.fpga_defaults + + self._command_q.put((command_set_nfee_fpga_defaults, [fpga_defaults], {})) - LOGGER.debug("Controller.n_fee_set_fpga_defaults: Put n_fee_set_fpga_defaults on the Queue.") + LOGGER.debug("Controller.n_fee_set_fpga_defaults: Put command_set_nfee_fpga_defaults on the Queue.") (cmd, response) = self._response_q.get() @@ -4164,15 +4565,25 @@

    Methods

    def n_fee_set_fpga_defaults(self)
    -
    +

    Loads the FPGA defaults from the Setup and commands the DPUProcessor to pass these defaults to the N-FEE.

    +

    Returns

    +

    The response from the DPU Processor after executing the command.

    Expand source code
    def n_fee_set_fpga_defaults(self):
    -    self._command_q.put((command_set_nfee_fpga_defaults, [], {}))
    +    """
    +    Loads the FPGA defaults from the Setup and commands the DPUProcessor to pass these defaults to the N-FEE.
     
    -    LOGGER.debug("Controller.n_fee_set_fpga_defaults: Put n_fee_set_fpga_defaults on the Queue.")
    +    Returns:
    +        The response from the DPU Processor after executing the command.
    +    """
    +    fpga_defaults = self._setup.camera.fee.fpga_defaults
    +
    +    self._command_q.put((command_set_nfee_fpga_defaults, [fpga_defaults], {}))
    +
    +    LOGGER.debug("Controller.n_fee_set_fpga_defaults: Put command_set_nfee_fpga_defaults on the Queue.")
     
         (cmd, response) = self._response_q.get()
     
    @@ -5145,10 +5556,10 @@ 

    Args

    class DPUInternals -(num_cycles: int, expected_last_packet_flags: List[int], dump_mode: bool = False, internal_sync: bool = False, dump_mode_int: bool = False, frame_number: int = -1, ccd_sides_enum: enum.Enum = None, sensor_sel_enum: enum.Enum = None) +(num_cycles: int, expected_last_packet_flags: List[int], dump_mode: bool = False, internal_sync: bool = False, dump_mode_int: bool = False, frame_number: int = -1, ccd_sides_enum: enum.Enum = None, sensor_sel_enum: enum.Enum = None, ccd_id_to_bin: List[int] = None)
    -

    DPUInternals(num_cycles: int, expected_last_packet_flags: List[int], dump_mode: bool = False, internal_sync: bool = False, dump_mode_int: bool = False, frame_number: int = -1, ccd_sides_enum: enum.Enum = None, sensor_sel_enum: enum.Enum = None)

    +

    DPUInternals(num_cycles: int, expected_last_packet_flags: List[int], dump_mode: bool = False, internal_sync: bool = False, dump_mode_int: bool = False, frame_number: int = -1, ccd_sides_enum: enum.Enum = None, sensor_sel_enum: enum.Enum = None, ccd_id_to_bin: List[int] = None)

    Expand source code @@ -5186,6 +5597,9 @@

    Args

    # Enumeration with the sensor_sel sensor_sel_enum: Enum = None + # Mapping of the CCD identifier to the binary representation (loaded from the Setup) + ccd_id_to_bin: List[int] = None + # The clear_error_flags shall be executed on every readout, i.e. every 200ms and 400ms pulse. clear_error_flags = False @@ -5193,6 +5607,34 @@

    Args

    # saved in the HDF5 file upon reception. slicing_num_cycles = 0 + # When in internal sync the ccd_readout_order is not used like in external sync mode. + # Each readout is done on the same CCD, i.e. the first CCD number in the ccd_readout_order list. + # Therefore, we will rotate this list on each readout in internal sync dump mode to guarantee + # each CCD is cleared out. + + # Initialise cycling of CCDs in internal sync dump mode to the default CCD numbering. + current_ccd_readout_order = CCD_NUMBERS + + # The cycle_count goes from [0 -> 3] to make sure that, during internal sync dump mode, we have cleared out + # all four CCDs. A clear-out cycle can only be interrupted when cycle_count == 0, at that time all commands + # on the queue will be executed. + cycle_count = 0 + + def reset_int_sync_dump_mode(self, ccd_numbers: list = None): + """ + Resets the cycle_count to zero (0) and the current ccd_readout_order to the given ccd_numbers. + When ccd_numbers is None the default CCD readout order will be used, i.e. CCD_NUMBERS, [1,2,3,4]. + + Args: + ccd_numbers: a list of four CCD numbers going from 1 to 4. + + """ + self.current_ccd_readout_order = ccd_numbers or CCD_NUMBERS + self.cycle_count = 0 + + def int_sync_cycle_dump_mode(self): + """Returns True if we are in internal sync dump mode.""" + return self.internal_sync and self.dump_mode and self.num_cycles < 0 def is_start_of_cycle(self): """ @@ -5206,10 +5648,7 @@

    Args

    Returns True if in the last readout in this cycle. Note that, when in internal sync mode, this method always returns True. """ - if self.internal_sync: - return True - else: - return self.frame_number == 3 + return True if self.internal_sync else self.frame_number == 3 def is_400ms_pulse(self): return self.frame_number == 0 @@ -5218,12 +5657,16 @@

    Args

    return self.frame_number in [1, 2, 3] def update(self, n_fee_state: NFEEState.StateTuple): - self.dump_mode = not bool(n_fee_state.digitise_en) + self.dump_mode = n_fee_state.ccd_mode_config == n_fee_mode.FULL_IMAGE_MODE and not bool(n_fee_state.digitise_en) self.internal_sync = bool(n_fee_state.sync_sel) self.expected_last_packet_flags = create_expected_last_packet_flags(n_fee_state, self.sensor_sel_enum)

    Class variables

    +
    var ccd_id_to_bin : List[int]
    +
    +
    +
    var ccd_sides_enum : enum.Enum
    @@ -5232,6 +5675,14 @@

    Class variables

    +
    var current_ccd_readout_order
    +
    +
    +
    +
    var cycle_count
    +
    +
    +
    var dump_mode : bool
    @@ -5267,6 +5718,20 @@

    Class variables

    Methods

    +
    +def int_sync_cycle_dump_mode(self) +
    +
    +

    Returns True if we are in internal sync dump mode.

    +
    + +Expand source code + +
    def int_sync_cycle_dump_mode(self):
    +    """Returns True if we are in internal sync dump mode."""
    +    return self.internal_sync and self.dump_mode and self.num_cycles < 0
    +
    +
    def is_200ms_pulse(self)
    @@ -5308,10 +5773,7 @@

    Methods

    Returns True if in the last readout in this cycle. Note that, when in internal sync mode, this method always returns True. """ - if self.internal_sync: - return True - else: - return self.frame_number == 3 + return True if self.internal_sync else self.frame_number == 3
    @@ -5331,6 +5793,34 @@

    Methods

    return self.frame_number == 0
    +
    +def reset_int_sync_dump_mode(self, ccd_numbers: list = None) +
    +
    +

    Resets the cycle_count to zero (0) and the current ccd_readout_order to the given ccd_numbers. +When ccd_numbers is None the default CCD readout order will be used, i.e. CCD_NUMBERS, [1,2,3,4].

    +

    Args

    +
    +
    ccd_numbers
    +
    a list of four CCD numbers going from 1 to 4.
    +
    +
    + +Expand source code + +
    def reset_int_sync_dump_mode(self, ccd_numbers: list = None):
    +    """
    +    Resets the cycle_count to zero (0) and the current ccd_readout_order to the given ccd_numbers.
    +    When ccd_numbers is None the default CCD readout order will be used, i.e. CCD_NUMBERS, [1,2,3,4].
    +
    +    Args:
    +        ccd_numbers: a list of four CCD numbers going from 1 to 4.
    +
    +    """
    +    self.current_ccd_readout_order = ccd_numbers or CCD_NUMBERS
    +    self.cycle_count = 0
    +
    +
    def update(self, n_fee_state: egse.dpu.dpu.StateTuple)
    @@ -5341,7 +5831,7 @@

    Methods

    Expand source code
    def update(self, n_fee_state: NFEEState.StateTuple):
    -    self.dump_mode = not bool(n_fee_state.digitise_en)
    +    self.dump_mode = n_fee_state.ccd_mode_config == n_fee_mode.FULL_IMAGE_MODE and not bool(n_fee_state.digitise_en)
         self.internal_sync = bool(n_fee_state.sync_sel)
         self.expected_last_packet_flags = create_expected_last_packet_flags(n_fee_state, self.sensor_sel_enum)
    @@ -6352,7 +6842,7 @@

    Raises

    class DPUProcessor -(transport: SpaceWireInterface, priority_queue: >, command_queue: >, response_queue: >) +(transport: SpaceWireInterface, priority_queue: >, command_queue: >, response_queue: >)

    The DPU Processor handles all interactions with the FEE. It reads the packets from the FEE @@ -6400,21 +6890,30 @@

    Raises

    self.register_map = RegisterMap("N-FEE") self._quit_event = multiprocessing.Event() - # These will be properly initialized when the register map is read from the N-FEE + # The following variables will be initialised in the run() method. - self._n_fee_state = NFEEState() + self._setup = None + self._dpu_internals = None + + # These will be properly initialized when the register map is read from the N-FEE. + self._n_fee_state = NFEEState() def run(self): + self._setup = load_setup() + if self._setup is None: + raise SetupError("Couldn't load the current Setup from the configuration manager.") + self._dpu_internals = DPUInternals( num_cycles=-1, expected_last_packet_flags=[False, False, False, False], dump_mode=False, internal_sync=False, frame_number=-1, - ccd_sides_enum=GlobalState.setup.camera.fee.ccd_sides.enum, - sensor_sel_enum=GlobalState.setup.camera.fee.sensor_sel.enum, + ccd_sides_enum=self._setup.camera.fee.ccd_sides.enum, + sensor_sel_enum=self._setup.camera.fee.sensor_sel.enum, + ccd_id_to_bin=self._setup.camera.fee.ccd_numbering.CCD_ID_TO_BIN, ) # The DPU Processor runs in a different process and since ZeroMQ Sockets are not @@ -6583,6 +7082,7 @@

    Raises

    if self._dpu_internals.num_cycles == 0: if self._dpu_internals.dump_mode_int: + self._dpu_internals.reset_int_sync_dump_mode() dump_mode_command = command_set_dump_mode_int_sync else: dump_mode_command = command_set_dump_mode @@ -6600,6 +7100,46 @@

    Raises

    self._transport, storage, origin_spw_data, start_time, mode, self.register_map, data_attr, self._dpu_internals, dist_sock) + # Read HK packet from N-FEE memory map + # see #2478 [https://github.com/IvS-KULeuven/plato-common-egse/issues/2478] + + time.sleep(0.012) # add 12ms to make sure HK data has been updated on the N-FEE + + with Timer("Read and process updated HK data"): + hk_data, timestamp = read_updated_hk_data(self._transport) + process_updated_hk_data(hk_data, timestamp, storage, origin_spw_data, + self._dpu_internals.frame_number, mon_sock, dist_sock) + + if self._dpu_internals.int_sync_cycle_dump_mode(): + LOGGER.warning("Cycling CCD readout in internal sync") + + # When we are in internal sync and dump mode, we need to cycle through the four CCDs, and + # we need an atomic block of four clear outs. + + internals = self._dpu_internals + internals.current_ccd_readout_order = rotate_list(internals.current_ccd_readout_order, 1) + internals.cycle_count += 1 + ccd_readout_order = _get_ccd_readout_order( + internals.current_ccd_readout_order, internals.ccd_id_to_bin + ) + # LOGGER.info(f"{internals.current_ccd_readout_order = }, {ccd_readout_order = }, " + # f"{internals.cycle_count = }") + _ = command_set_readout_order(self._transport, self.register_map, ccd_readout_order) + + # We agreed to have atomic blocks of 4 clear-outs such that all four CCDs would always + # be dumped. So, whenever we are within one such atomic block, don't execute any DPU + # commands. + + if internals.cycle_count < 4: + LOGGER.debug( + f"[1] {internals.current_ccd_readout_order = }, {ccd_readout_order = }, " + f"{internals.cycle_count = }") + else: + internals.cycle_count = 0 + LOGGER.debug( + f"[2] {internals.current_ccd_readout_order = }, {ccd_readout_order = }, " + f"{internals.cycle_count = }") + except NoBytesReceivedError as exc: # LOGGER.debug(f"No bytes received: {exc}") pass @@ -6641,6 +7181,13 @@

    Raises

    self._priority_q, self._response_q, self._n_fee_state.get_state(), self._dpu_internals, self.register_map) + # When we are in internal sync dump mode, we need atomic blocks of 4 readouts such that all four + # CCDs will be cleared out. The cycle count goes from [0 -> 3] so, we only send commands when + # cycle count == 0. + # LOGGER.debug(f"{self._dpu_internals.cycle_count = }") + if self._dpu_internals.int_sync_cycle_dump_mode() and self._dpu_internals.cycle_count != 0: + continue + # Then, we might want to send some RMAP commands ------------------------------- # When we are in the 2s RMAP window, send the commands. @@ -6722,7 +7269,7 @@

    Raises

    LOGGER.debug(f"Timecode received {packet=}") - while time.perf_counter() < start_time + 4.2: + while time.perf_counter() < start_time + 4.2: # FIXME: assuming N-FEE in external sync when starting... terminator, packet = self._transport.read_packet(timeout=200) if packet is None: msg = f"time passed {time.perf_counter() - start_time:0.3f}" @@ -6784,7 +7331,7 @@

    Methods

    LOGGER.debug(f"Timecode received {packet=}") - while time.perf_counter() < start_time + 4.2: + while time.perf_counter() < start_time + 4.2: # FIXME: assuming N-FEE in external sync when starting... terminator, packet = self._transport.read_packet(timeout=200) if packet is None: msg = f"time passed {time.perf_counter() - start_time:0.3f}" @@ -6827,14 +7374,19 @@

    Methods

    def run(self):
     
    +    self._setup = load_setup()
    +    if self._setup is None:
    +        raise SetupError("Couldn't load the current Setup from the configuration manager.")
    +
         self._dpu_internals = DPUInternals(
             num_cycles=-1,
             expected_last_packet_flags=[False, False, False, False],
             dump_mode=False,
             internal_sync=False,
             frame_number=-1,
    -        ccd_sides_enum=GlobalState.setup.camera.fee.ccd_sides.enum,
    -        sensor_sel_enum=GlobalState.setup.camera.fee.sensor_sel.enum,
    +        ccd_sides_enum=self._setup.camera.fee.ccd_sides.enum,
    +        sensor_sel_enum=self._setup.camera.fee.sensor_sel.enum,
    +        ccd_id_to_bin=self._setup.camera.fee.ccd_numbering.CCD_ID_TO_BIN,
         )
     
         # The DPU Processor runs in a different process and since ZeroMQ Sockets are not
    @@ -7003,6 +7555,7 @@ 

    Methods

    if self._dpu_internals.num_cycles == 0: if self._dpu_internals.dump_mode_int: + self._dpu_internals.reset_int_sync_dump_mode() dump_mode_command = command_set_dump_mode_int_sync else: dump_mode_command = command_set_dump_mode @@ -7020,6 +7573,46 @@

    Methods

    self._transport, storage, origin_spw_data, start_time, mode, self.register_map, data_attr, self._dpu_internals, dist_sock) + # Read HK packet from N-FEE memory map + # see #2478 [https://github.com/IvS-KULeuven/plato-common-egse/issues/2478] + + time.sleep(0.012) # add 12ms to make sure HK data has been updated on the N-FEE + + with Timer("Read and process updated HK data"): + hk_data, timestamp = read_updated_hk_data(self._transport) + process_updated_hk_data(hk_data, timestamp, storage, origin_spw_data, + self._dpu_internals.frame_number, mon_sock, dist_sock) + + if self._dpu_internals.int_sync_cycle_dump_mode(): + LOGGER.warning("Cycling CCD readout in internal sync") + + # When we are in internal sync and dump mode, we need to cycle through the four CCDs, and + # we need an atomic block of four clear outs. + + internals = self._dpu_internals + internals.current_ccd_readout_order = rotate_list(internals.current_ccd_readout_order, 1) + internals.cycle_count += 1 + ccd_readout_order = _get_ccd_readout_order( + internals.current_ccd_readout_order, internals.ccd_id_to_bin + ) + # LOGGER.info(f"{internals.current_ccd_readout_order = }, {ccd_readout_order = }, " + # f"{internals.cycle_count = }") + _ = command_set_readout_order(self._transport, self.register_map, ccd_readout_order) + + # We agreed to have atomic blocks of 4 clear-outs such that all four CCDs would always + # be dumped. So, whenever we are within one such atomic block, don't execute any DPU + # commands. + + if internals.cycle_count < 4: + LOGGER.debug( + f"[1] {internals.current_ccd_readout_order = }, {ccd_readout_order = }, " + f"{internals.cycle_count = }") + else: + internals.cycle_count = 0 + LOGGER.debug( + f"[2] {internals.current_ccd_readout_order = }, {ccd_readout_order = }, " + f"{internals.cycle_count = }") + except NoBytesReceivedError as exc: # LOGGER.debug(f"No bytes received: {exc}") pass @@ -7061,6 +7654,13 @@

    Methods

    self._priority_q, self._response_q, self._n_fee_state.get_state(), self._dpu_internals, self.register_map) + # When we are in internal sync dump mode, we need atomic blocks of 4 readouts such that all four + # CCDs will be cleared out. The cycle count goes from [0 -> 3] so, we only send commands when + # cycle count == 0. + # LOGGER.debug(f"{self._dpu_internals.cycle_count = }") + if self._dpu_internals.int_sync_cycle_dump_mode() and self._dpu_internals.cycle_count != 0: + continue + # Then, we might want to send some RMAP commands ------------------------------- # When we are in the 2s RMAP window, send the commands. @@ -7599,7 +8199,6 @@

    Index

  • egse.dpu.dpu_cs
  • egse.dpu.dpu_ui
  • egse.dpu.fitsgen
  • -
  • egse.dpu.fitsgen_test
  • egse.dpu.fitsgen_ui
  • egse.dpu.hdf5_model
  • egse.dpu.hdf5_ui
  • @@ -7617,10 +8216,13 @@

    Index

  • process_high_priority_commands
  • process_hk_packet
  • process_timecode
  • +
  • process_updated_hk_data
  • read_and_process_data_packets
  • read_hk_packet
  • read_timecode
  • +
  • read_updated_hk_data
  • register_to_storage_manager
  • +
  • rotate_list
  • save_format_version
  • save_num_cycles
  • save_obsid
  • @@ -7684,18 +8286,23 @@

    DPUInte
  • DPUInternals

  • -

    The following codes are set up with this two way construct:

    +

    The following codes are set up with this two-way construct:

    • Terminator Codes
    • Extension Codes
    • ESL Error Codes
    • +
    • ESL subsidiary API Error Codes
    • RMAP Error Codes
    @@ -65,22 +66,32 @@

    Why do we do this?

    logger.debug(f"We received an error code {constants.esl_error_codes[err]} [{err}]") ``` -The following codes are set up with this two way construct: +The following codes are set up with this two-way construct: * Terminator Codes * Extension Codes * ESL Error Codes +* ESL subsidiary API Error Codes * RMAP Error Codes """ -import sys import logging +import sys logger = logging.getLogger(__name__) module = sys.modules[__name__] +__all__ = [ + 'esl_api_error_codes', + 'esl_error_codes', + 'esl_extension_codes', + 'esl_link_mode', + 'esl_rmap_error_codes', + 'esl_terminator_codes', +] + # ESL Error Codes and Constants -------------------------------------------------------------------- # DSI event bits @@ -109,6 +120,13 @@

    Why do we do this?

    ESL_LINK_MODE_LEGACY = 0x04 ESL_LINK_MODE_MASTER = 0x06 +esl_link_mode = { + 0x01: "DISABLED", + 0x02: "NORMAL", + 0x04: "LEGACY", + 0x06: "MASTER" +} + # Error Reporting ESL_ER_REPORT_PARITY_ERROR = ESL_DSI_PARITY_ERROR @@ -148,6 +166,7 @@

    Why do we do this?

    except AttributeError: setattr(module, name, value) +del esl_terminator_names # Define the ESL Extension Codes @@ -196,6 +215,7 @@

    Why do we do this?

    except AttributeError: setattr(module, name, value) +del esl_extension_names # Define the ESL Error Codes @@ -220,6 +240,61 @@

    Why do we do this?

    except AttributeError: setattr(module, name, value) +del esl_error_names + +# Define subsidiary ESL API error codes + +esl_api_error_codes = { + -1: "ESL_API_ERROR_RECFILE_OPEN", # Couldn't open recording file + -2: "ESL_API_ERROR_RECFILE_WRITE", # record_file write failed + -3: "ESL_API_ERROR_LOGFILE_OPEN", # Couldn't open logging file + -4: "ESL_API_ERROR_LOGFILE_WRITE", # log_file write failed + -10: "ESL_API_ERROR_RECEIVER_TIMEOUT", # we have a network timeout + -11: "ESL_API_ERROR_RECEIVER_SHUTDOWN", # peer has performed an orderly shutdown + -12: "ESL_API_ERROR_IO_ERROR", # we have an IO error + -15: "ESL_API_ERROR_SAVEBUF_OVERFLOW_SAVE", # Saving the read_packet_full() save_buffer failed + -16: "ESL_API_ERROR_SAVEBUF_OVERFLOW_RESTORE", # Restoring the read_packet_full() save_buffer failed + -17: "ESL_API_ERROR_FUNCTION_NOT_SUPPORTED", # Device does not support the requested function + -18: "ESL_API_NETWORK", # Error reading / writing to/from the device + -19: "ESL_API_NETWORK_FORMAT_ERROR", # Error understanding received packet + -20: "ESL_API_REQUEST_TOO_LARGE", # The I/O request can't be fulfilled by the hardware + -21: "ESL_API_SEQUENCE_ERROR", # Didn't receive expected notification from the hardware + -22: "ESL_API_RESPONSE_TOO_SMALL", # Response from the device didn't contain enough data + -23: "ESL_API_RESPONSE_MISMATCH", # Response does not match I/O request + -24: "ESL_API_MODULE_NOT_PRESENT", # Module not present + -25: "ESL_API_PARAMETER_RANGE_INCORRECT", # Parameter not in range + -26: "ESL_API_FILE_NOT_PRESENT", # Requested file is not present + -27: "ESL_API_EINTR", # EINTR occurred + -28: "ESL_API_LINK_INCORRECT", # Link number is incorrect + -29: "ESL_API_INCORRECT_DEVICE", # Connecting to a device which does not support functionality + -30: "ESL_API_MEMORY", # Unable to allocate memory + -31: "ESL_API_HOST_UNRESOLVABLE", # Unable to resolve host + -32: "ESL_API_HOST_UNRESPONSIVE", # Unable to connect to host + -33: "ESL_API_WAVEFORM_DIR_CREATE", # Unable to create waveform directory + -34: "ESL_API_ZERO_READ", # asked to read zero bytes + -35: "ESL_API_SET_OPTION_FILE", # Asked to set an option when playing back from file + -36: "ESL_API_INVALID_DEVICE", # Device is not supported by API + -37: "ESL_API_FILE_MOVE", # Unable to move file into place + -38: "ESL_API_INVALID_FILE", # Unable to open file + -39: "ESL_API_CALLBACK_RETURN", # Callback has asked for a return + -40: "ESL_API_FILELIST_EMPTY", # List of files given is empty + -41: "ESL_API_UNKNOWN_SYSTEM_TYPE", # Unknown type + -42: "ESL_API_NOT_KNOWN", # API returned 0 as an error should (should not happen) + -43: "ESL_API_EXE_START_FAILED", # Cannot start executable + -44: "ESL_API_NO_CONNECTION", # Link Not established + -45: "ESL_API_INVALID_LINK", # Invalid Link selected +} + +esl_api_error_codes_names = dict([(v, k) for k, v in esl_api_error_codes.items()]) + +for name, value in esl_api_error_codes_names.items(): + try: + if getattr(module, name) != value: + logger.error(f"module {__name__} is trying to overwrite the constant {name} with a new value {value}.") + except AttributeError: + setattr(module, name, value) + +del esl_api_error_codes_names # RMAP Error Codes and Constants ------------------------------------------------------------------- @@ -265,7 +340,9 @@

    Why do we do this?

    getattr(module, name) logger.error(f"module {__name__} is trying to overwrite the constant {name} with a new value.") except AttributeError: - setattr(module, name, value)
    + setattr(module, name, value) + +del esl_rmap_error_names
    diff --git a/docs/api/egse/dsi/esl.html b/docs/api/egse/dsi/esl.html index 561e273..2747595 100644 --- a/docs/api/egse/dsi/esl.html +++ b/docs/api/egse/dsi/esl.html @@ -88,31 +88,22 @@

    Module egse.dsi.esl

    import ctypes import logging import time +import typing from contextlib import contextmanager -from ctypes import POINTER -from ctypes import Structure from ctypes import c_char_p from ctypes import c_int -from ctypes import c_size_t -from ctypes import c_ssize_t -from ctypes import c_uint32 -from ctypes import c_uint64 -from ctypes import c_uint8 -from ctypes import c_void_p -from ctypes import c_void_p as c_int_p -from ctypes import cdll -from pathlib import Path from typing import Tuple -from egse.config import find_file +import egse from egse.dsi import constants from egse.dsi.constants import esl_error_codes from egse.dsi.constants import esl_extension_codes +from egse.dsi.constants import esl_link_mode from egse.dsi.constants import esl_terminator_codes from egse.settings import Settings -from egse.spw import HousekeepingPacket -from egse.system import get_os_name -from egse.system import get_os_version + +if typing.TYPE_CHECKING: + from egse.dsi._libesl import ESL logger = logging.getLogger(__name__) @@ -123,115 +114,12 @@

    Module egse.dsi.esl

    rx_buffer = ctypes.create_string_buffer(dsi_settings.RX_BUFFER_LENGTH) tx_buffer = ctypes.create_string_buffer(dsi_settings.TX_BUFFER_LENGTH) -# Depending on the OS, and the OS version, we load the dynamic library from a specific location -# Only some OS/OS-version combinations are supported. -if get_os_name() == 'macos': - in_dir = 'lib/macOS' -elif get_os_name() == 'debian': - in_dir = 'lib/Debian' -elif get_os_name() == 'centos': - in_dir = 'lib/CentOS-7' if get_os_version().startswith('7') else 'lib/CentOS-8' -elif get_os_name() == 'ubuntu': - in_dir = 'lib/Ubuntu-20' if get_os_version().startswith('20') else 'lib/Linux' -else: - in_dir = None - -dylib_filename = Path(dsi_settings.ESL_DYLIB_FILENAME) - -logger.debug(f"Locating shared library {dylib_filename} in dir '{in_dir}'") - -dylib_filename = find_file(dylib_filename, in_dir=in_dir) - -logger.debug(f"Loading shared library: {dylib_filename}") - -if not dylib_filename: - raise FileNotFoundError(f"Could not find dynamic library: {dylib_filename}") -libesl = cdll.LoadLibrary(dylib_filename) - - -class ESL(Structure): - _fields_ = [ - ("sock", c_int), - ("tx_buffer", c_char_p), - ("tx_buffer_length", c_size_t), - ("tx_buffer_content", c_size_t), - ("rx_buffer", c_char_p), - ("rx_buffer_length", c_size_t), - ("rx_buffer_content", c_ssize_t), - ("rx_buffer_ptr", c_int), - ("rx_state", c_int), - ("rx_count", c_int), - ("rx_param", c_int), - ("rx_timeout", c_int), - ("rx_final_flags", c_int), - ("extn_count", c_int), - ("extn_size", c_int), - ("extn_cmd", c_int), - ("extn_byte", c_int), - ("extn_link", c_int), - ("cmd_buffer", c_char_p), - ("time_buffer", c_char_p), - - ("special_cb", c_void_p), - ("extension_cb", c_void_p), - - ("number_of_links", c_int), - ("number_of_slots", c_int), - ("slot_content", c_int_p), - ("record_file", c_void_p), - ("log_file", c_void_p), - ("max_length", c_int), - ("log_rx_port", c_int), - ("log_tx_port", c_int), - - ("ram_io", c_int), - ("current_port", c_int), - - ("check_record_writes", c_int), - ("total_raw_bytes_received", c_uint64), - - ("options", c_char_p), - ("option", c_int), - ("ESL_error_errno", c_int), - - ("rx_timeout_returns_error", c_int), - - ("max_dumps", c_uint32), - ("context", c_void_p), - - ("rxinfo", c_uint32), - - ("read_calls", c_uint64), - ("read_io_calls", c_uint64), - ("file", c_int), - ("filesize", c_uint64), - ("recordsize", c_uint64), - - ("ram_io_int", c_int), - - ("eintr_enabled", c_int), - ("timezero", c_size_t), - ("timedata", c_uint64), - ("timedata_ns", c_uint32), - - ("partialframesz", c_size_t), - ("partialframe_offset", c_size_t), - ("system_type", c_int), - ("auto_flush", c_int), - ("live", c_int), - - ("epollfd", c_int), - ] - - -esl_p = POINTER(ESL) - class ESLError(Exception): pass -def esl_read_packet(esl_link: ESL, timeout: int = None) -> Tuple[int, bytes]: +def esl_read_packet(esl_link: 'ESL', timeout: int = None) -> Tuple[int, bytes]: """ Reads a full packet from the SpaceWire link. @@ -254,7 +142,7 @@

    Module egse.dsi.esl

    esl_link, rx_buffer, dsi_settings.RX_BUFFER_LENGTH, terminator_p, constants.ESL_RETURN_EXTENSION_DATA | constants.ESL_RETURN_SPECIAL_DATA, timeout=timeout) - logger.log(0, f"Number of bytes received: {bytes_received}") + # logger.debug(f"Number of bytes received: {bytes_received}") if bytes_received < 0: return bytes_received, bytes() @@ -263,7 +151,7 @@

    Module egse.dsi.esl

    @contextmanager -def esl_connection(dsi_address: str) -> ESL: +def esl_connection(dsi_address: str) -> 'ESL': """ Context Manager that opens a EtherSpaceLink connection with the DSI (Diagnostic SpaceWire Interface). @@ -282,7 +170,7 @@

    Module egse.dsi.esl

    esl_close_connection(esl_link) -def esl_open_connection(dsi_address: str) -> ESL: +def esl_open_connection(dsi_address: str) -> 'ESL': """ Open a connection to the EtherSpaceLink DSI on the given IP address. This function will keep trying to connect for 10 seconds before aborting @@ -301,13 +189,14 @@

    Module egse.dsi.esl

    esl_link = None while retry: - esl_link = libesl_open(c_char_p(dsi_address.encode())) + esl_link = egse.dsi._libesl.libesl_open(c_char_p(dsi_address.encode())) if esl_link: break else: time.sleep(0.5) # wait half a second before trying again logger.info(f"Trying to connect to {dsi_address}, {retry / 2.0} sec before shutdown.") retry -= 1 + if not esl_link: raise ESLError(f"Couldn't open connection to DSI on {dsi_address}.") @@ -328,7 +217,7 @@

    Module egse.dsi.esl

    Returns: Nothing """ - libesl_close(esl_link) + egse.dsi._libesl.libesl_close(esl_link) logger.info("EtherSpaceLink connection closed successfully.") @@ -342,7 +231,7 @@

    Module egse.dsi.esl

    Returns: 0 on success, !0 otherwise """ - result = libesl_flush(esl_link) + result = egse.dsi._libesl.libesl_flush(esl_link) # We don't want this to raise an exception, the result value should be checked by the caller instead. # if result: # raise ESLError( @@ -352,7 +241,7 @@

    Module egse.dsi.esl

    return result -def esl_configure(esl_link: ESL, +def esl_configure(esl_link: 'ESL', active_link: int = 1, speed: int = 50, mode: int = constants.ESL_LINK_MODE_NORMAL, report: int = 0): """ @@ -378,21 +267,42 @@

    Module egse.dsi.esl

    Nothing """ status = esl_set_active_link(esl_link, active_link) + logger.info(f"esl_set_active_link({active_link}) -> {status = }") status = esl_set_speed(esl_link, speed) - status = esl_set_mode(esl_link, mode) + logger.info(f"esl_set_speed({speed}) -> {status = }") + if report: status = esl_er_enable_reporting(esl_link, report) + logger.info(f"esl_er_enable_reporting(0b{report:b}) -> {status = }") + + status = esl_set_mode(esl_link, mode) + logger.info(f"esl_set_mode({esl_link_mode[mode]}={mode}) -> {status = }") + + status = esl_flush(esl_link) + logger.info(f"esl_flush() -> {status = }") + + +def esl_set_active_link(esl_link, active_link): + return egse.dsi._libesl.libesl_set_active_link(esl_link, active_link) + + +def esl_get_active_link(esl_link): + return esl_link.contents.current_port + + +def esl_is_link_connected(esl_link): + return egse.dsi._libesl.libesl_link_connected(esl_link) def esl_set_speed(esl_link, speed): - result = libesl_set_speed(esl_link, speed) + result = egse.dsi._libesl.libesl_set_speed(esl_link, speed) if result: raise ESLError("Could not set speed to {}, ESL error code={} [{}]".format(speed, esl_error_codes[ esl_link.contents.ESL_error], esl_link.contents.ESL_error)) return result -def esl_set_mode(esl_link: ESL, mode: int) -> int: +def esl_set_mode(esl_link: 'ESL', mode: int) -> int: """ Set the operating mode of the currently active SpaceWire link. @@ -408,15 +318,14 @@

    Module egse.dsi.esl

    0 if the request has been queued, not 0 if not. """ - result = libesl_set_mode(esl_link, mode) + result = egse.dsi._libesl.libesl_set_mode(esl_link, mode) if result: - raise ESLError( - "Could not set mode, ESL error code={} [{}]".format(esl_error_codes[esl_link.contents.ESL_error], - esl_link.contents.ESL_error)) + raise ESLError(f"Could not set mode {esl_link_mode[mode]}.") + return result -def esl_send_timecode(esl_link: ESL, timecode: int) -> int: +def esl_send_timecode(esl_link: 'ESL', timecode: int) -> int: """ Send a timecode over the SpaceWire link. @@ -430,7 +339,7 @@

    Module egse.dsi.esl

    0 if the request has been queued, not 0 if not. """ - result = libesl_send_timecode(esl_link, timecode) + result = egse.dsi._libesl.libesl_send_timecode(esl_link, timecode) if result: raise ESLError( @@ -438,7 +347,7 @@

    Module egse.dsi.esl

    f"[{esl_link.contents.ESL_error}]" ) - libesl_flush(esl_link) + result = egse.dsi._libesl.libesl_flush(esl_link) return result @@ -447,7 +356,11 @@

    Module egse.dsi.esl

    return esl_link.contents.rx_timeout -def esl_get_receive_speed(esl_link: ESL) -> int: +def esl_set_rx_timeout(esl_link, timeout: int): + esl_link.contents.rx_timeout = timeout + + +def esl_get_receive_speed(esl_link: 'ESL') -> int: """ Gets the receive speed of the currently active link. @@ -458,43 +371,62 @@

    Module egse.dsi.esl

    esl_link (ESL): the ESL structure that defines the connection to the DSI Returns: - the speed of the active link in Mbtis/s. In case of an error a value < 0 will be returned. + the speed of the active link in Mbits/s. In case of an error a value < 0 will be returned. """ - return libesl_get_receive_speed(esl_link) + return egse.dsi._libesl.libesl_get_receive_speed(esl_link) def esl_set_log_file(esl_link, filename): - result = libesl_set_log_file(esl_link, c_char_p(filename.encode())) + result = egse.dsi._libesl.libesl_set_log_file(esl_link, c_char_p(filename.encode())) if result: - raise ESLError(f"Could not write to or open record file {filename}.") + raise ESLError(f"Could not write to or open log file {filename}.") return result def esl_set_record_file(esl_link, filename): - result = libesl_set_record_file(esl_link, c_char_p(filename.encode())) + result = egse.dsi._libesl.libesl_set_record_file(esl_link, c_char_p(filename.encode())) if result: - raise ESLError(f"Could not write to or open log file {filename}.") + raise ESLError(f"Could not write to or open record file {filename}.") return result def esl_get_manufacturer_string(esl_link): - return libesl_get_manufacturer_string(esl_link).decode() + return egse.dsi._libesl.libesl_get_manufacturer_string(esl_link).decode() def esl_get_product_string(esl_link): - return libesl_get_product_string(esl_link).decode() + return egse.dsi._libesl.libesl_get_product_string(esl_link).decode() + + +def esl_get_esl_error(esl_link): + return egse.dsi._libesl.libesl_get_esl_error(esl_link) + + +def esl_get_hwa(esl_link): + hwa = b'012345' # Pre-allocate the character buffer + egse.dsi._libesl.libesl_get_hwa(esl_link, hwa) + return hwa + + +def esl_get_serial_number(esl_link): + hwa = esl_get_hwa(esl_link) + return egse.dsi._libesl.libesl_hwa_to_serial_number_string(hwa) + + +def esl_get_number_of_links(esl_link): + return egse.dsi._libesl.libesl_get_number_of_links(esl_link) def esl_read_packet_full(esl_link, buffer, buffer_length, rx_terminator, special_data_action, timeout: int = None): if timeout: saved_timeout = esl_get_rx_timeout(esl_link) - esl_set_rx_timeout(esl_link, timeout) + egse.dsi._libesl.libesl_set_rx_timeout(esl_link, timeout) - result = libesl_read_packet_full(esl_link, buffer, buffer_length, rx_terminator, special_data_action) + result = egse.dsi._libesl.libesl_read_packet_full(esl_link, buffer, buffer_length, rx_terminator, special_data_action) if timeout: - esl_set_rx_timeout(esl_link, saved_timeout) + egse.dsi._libesl.libesl_set_rx_timeout(esl_link, saved_timeout) # This error handling is (or should be) done in the calling application, see for example egse.feesim.py # if result == -1: @@ -506,7 +438,7 @@

    Module egse.dsi.esl

    return result -def esl_write_packet(esl_link: ESL, buffer, buffer_length: int, tx_terminator: int) -> int: +def esl_write_packet(esl_link: 'ESL', buffer, buffer_length: int, tx_terminator: int) -> int: """ Queue data for transmission over the SpaceWire cable. If there is no room left in the buffer, the buffer is transmitted. @@ -521,30 +453,16 @@

    Module egse.dsi.esl

    tx_terminator: additional metadata about the frame we are transmitting (EOP, EEP, PART_EOP_EEP, EXTN) Returns: - return_code: 0 on success, < 0 otherwise + return_code: 0 on success, < 0 when an error occurred. """ - # logger.debug( - # f"Calling esl_write_packet: " - # f"buffer={pp_packet(buffer)}, buffer_length={buffer_length}, tx_terminator=0x{tx_terminator:x} " - # f"[{get_terminator_name(tx_terminator) or get_extension_name(tx_terminator)}]." - # ) - - result = libesl_write_packet(esl_link, buffer, buffer_length, tx_terminator) - - # This error handling is (or should be) done in the calling application, see for example egse.feesim.py - # if result == -1: - # raise ESLError( - # f"Could not write the packet, " - # f"ESL error code = {esl_error_codes[esl_link.contents.ESL_error]} [{esl_link.contents.ESL_error}]" - # ) - # logger.debug(f"Returning from esl_write_packet: buffer={pp_packet(buffer)}, result = {result}.") + result = egse.dsi._libesl.libesl_write_packet(esl_link, buffer, buffer_length, tx_terminator) return result def esl_er_enable_reporting(esl_link, flags): - result = libesl_er_enable_reporting(esl_link, flags) + result = egse.dsi._libesl.libesl_er_enable_reporting(esl_link, flags) if result: raise ESLError( f"Could not enable error reporting, " @@ -553,7 +471,7 @@

    Module egse.dsi.esl

    return result -def esl_print_info(esl_link: ESL) -> None: +def esl_print_info(esl_link: 'ESL') -> None: """ Prints information about the connected device to the console. @@ -563,16 +481,12 @@

    Module egse.dsi.esl

    Returns: nothing """ - print("Manufacturer {}".format(esl_get_manufacturer_string(esl_link))) - print("Product {}".format(esl_get_product_string(esl_link))) - print("Number of links {}".format(esl_get_number_of_links(esl_link))) - - # Pre-allocate the character buffer - - hwa = b'012345' + print(f"Manufacturer {esl_get_manufacturer_string(esl_link)}") + print(f"Product {esl_get_product_string(esl_link)}") + print(f"Number of links {esl_get_number_of_links(esl_link)} [active port={esl_get_active_link(esl_link)}]") - esl_get_hwa(esl_link, hwa) - serial_number = esl_hwa_to_serial_number_string(hwa) + hwa = esl_get_hwa(esl_link) + serial_number = esl_get_serial_number(esl_link) print(f"Serial number {serial_number}") print(f"Hardware Address 0x{hwa[0]:02X}-{hwa[1]:02X}-{hwa[2]:02X}-{hwa[3]:02X}-{hwa[4]:02X}-{hwa[5]:02X}") @@ -596,102 +510,6 @@

    Module egse.dsi.esl

    #print("id {}".format(esl.contents.id)) -esl_get_version = libesl.EtherSpaceLink_get_version -esl_get_version.argtypes = [] -esl_get_version.restype = c_char_p - -libesl_open = libesl.EtherSpaceLink_open -libesl_open.argtypes = [c_char_p] -libesl_open.restype = esl_p - -libesl_close = libesl.EtherSpaceLink_close -libesl_close.argtypes = [esl_p] - -libesl_flush = libesl.EtherSpaceLink_flush -libesl_flush.argtypes = [esl_p] - -esl_shutdown = libesl.EtherSpaceLink_shutdown -esl_shutdown.argtypes = [esl_p] - -esl_link_connected = libesl.EtherSpaceLink_link_connected -esl_link_connected.argtypes = [esl_p] -esl_link_connected.restype = c_int - -esl_set_active_link = libesl.EtherSpaceLink_set_active_link -esl_set_active_link.argtypes = [esl_p, c_int] -esl_set_active_link.restype = c_int - -libesl_set_speed = libesl.EtherSpaceLink_set_speed -libesl_set_speed.argtypes = [esl_p, c_int] -libesl_set_speed.restype = c_int - -libesl_set_mode = libesl.EtherSpaceLink_set_mode -libesl_set_mode.argtypes = [esl_p, c_int] -libesl_set_mode.restype = c_int - -libesl_send_timecode = libesl.EtherSpaceLink_send_timecode -libesl_set_mode.argtypes = [esl_p, c_uint8] -libesl_set_mode.restype = c_int - -esl_set_rx_timeout = libesl.EtherSpaceLink_set_rx_timeout -esl_set_rx_timeout.argtypes = [esl_p, c_int] - -esl_set_rx_timeout_action = libesl.EtherSpaceLink_set_rx_timeout_action -esl_set_rx_timeout_action.argtypes = [esl_p, c_int] - -libesl_set_log_file = libesl.EtherSpaceLink_set_log_file -libesl_set_log_file.argtypes = [esl_p, c_char_p] -libesl_set_log_file.restype = c_int - -libesl_set_record_file = libesl.EtherSpaceLink_set_record_file -libesl_set_record_file.argtypes = [esl_p, c_char_p] -libesl_set_record_file.restype = c_int - -esl_request_link_status = libesl.EtherSpaceLink_request_link_status -esl_request_link_status.argtypes = [esl_p] -esl_request_link_status.restype = c_int - -libesl_get_receive_speed = libesl.EtherSpaceLink_get_receive_speed -libesl_get_receive_speed.argtypes = [esl_p] -libesl_get_receive_speed.restype = c_int - -esl_get_esl_error = libesl.EtherSpaceLink_get_error -esl_get_esl_error.argtypes = [esl_p] -esl_get_esl_error.restype = c_int - -esl_get_number_of_links = libesl.EtherSpaceLink_get_number_of_links -esl_get_number_of_links.argtypes = [esl_p] -esl_get_number_of_links.restype = c_int - -libesl_get_manufacturer_string = libesl.EtherSpaceLink_get_manufacturer_string -libesl_get_manufacturer_string.argtypes = [esl_p] -libesl_get_manufacturer_string.restype = c_char_p - -libesl_get_product_string = libesl.EtherSpaceLink_get_product_string -libesl_get_product_string.argtypes = [esl_p] -libesl_get_product_string.restype = c_char_p - -esl_get_hwa = libesl.EtherSpaceLink_get_HWA -esl_get_hwa.argtypes = [esl_p, c_char_p] -esl_get_hwa.restype = c_int - -esl_hwa_to_serial_number_string = libesl.EtherSpaceLink_HWA_to_serial_number_string -esl_hwa_to_serial_number_string.argtypes = [c_char_p] -esl_hwa_to_serial_number_string.restype = c_char_p - -libesl_read_packet_full = libesl.EtherSpaceLink_read_packet_full -libesl_read_packet_full.argtypes = [esl_p, c_void_p, c_int, c_int_p, c_int] -libesl_read_packet_full.restype = c_int - -libesl_write_packet = libesl.EtherSpaceLink_write_packet -libesl_write_packet.argtypes = [esl_p, c_void_p, c_size_t, c_uint32] -libesl_write_packet.restype = c_int - -libesl_er_enable_reporting = libesl.EtherSpaceLink_ER_enable_reporting -libesl_er_enable_reporting.argtypes = [esl_p, c_int] -libesl_er_enable_reporting.restype = c_int - - # Helper Functions --------------------------------------------------------------------------------- @@ -735,64 +553,6 @@

    Module egse.dsi.esl

    DATA_HK_PROTOCOL_ID = 0xF0 -def pretty_print_packet(packet) -> str: - - from egse.fee import is_hk_data_packet - - msg = f"packet is of type {type(packet)}, with length {len(packet)}\n" - - # When the packet is of type ctypes.c_char_Array_24 (i.e. ctypes array of c_char) - # then convert the packet into a bytes object. - - if hasattr(packet, 'raw'): - packet = packet.raw - - # First check if this is a timecode packet because this is only 2 bytes long and reading the instruction field - # will result in an IndexError otherwise. - - if is_timecode(packet): - msg += ( - f"Time code received: 0x{packet[1]:02x}\n" - ) - elif get_protocol_id(packet) == RMAP_PROTOCOL_ID: - import egse.dsi.rmap - instruction_field = egse.dsi.rmap.get_instruction_field(packet) - if instruction_field == 0x4C: - msg += pretty_print_read_request_packet(packet) - elif instruction_field == 0x4C & 0x3F: - msg += pretty_print_read_request_reply_packet(packet) - elif instruction_field == 0x7C: - msg += pretty_print_verified_write_request_packet(packet) - elif instruction_field == 0x6C: - msg += pretty_print_unverified_write_request_packet(packet) - elif instruction_field == 0x3C & 0x3F: - msg += pretty_print_write_request_reply_packet(packet) - else: - msg += ( - f"RMAP packet (to-be-implemented)\n" - f'{pp_packet(packet)}' - ) - elif get_protocol_id(packet) == CCSDS_PROTOCOL_ID: - msg += ( - f"CCSDS Packet\n" - f"Packet: {' '.join([hex(x) for x in packet[:min(80, len(packet))]])} [max. 80 bytes printed]." - ) - elif get_protocol_id(packet) == DATA_HK_PROTOCOL_ID and is_hk_data_packet(packet): - # FIXME: this puts an unnecessary dependency on the esl.py module. The HousekeepingPacket is PLATO specific - # and should not be in the esl.py module - msg += ( - f"HK Packet:\n" - f"{HousekeepingPacket(packet)}" - ) - else: - msg += ( - f"Extended Protocol Identifier is not supported.\n" - f"Packet: {' '.join([hex(x) for x in packet[:min(80, len(packet))]])} [max. 80 bytes printed]." - ) - - return msg - - def pretty_print_read_request_packet(packet): msg = ( f"RMAP Read Request ({len(packet)} bytes)\n" @@ -938,7 +698,7 @@

    Functions

    This also flushes and closes the log and record files if they were used.

    Args

    -
    esl_link : ESL
    +
    esl_link : ESL
    the ESL structure that defines the connection to the DSI

    Returns

    @@ -959,12 +719,12 @@

    Returns

    Returns: Nothing """ - libesl_close(esl_link) + egse.dsi._libesl.libesl_close(esl_link) logger.info("EtherSpaceLink connection closed successfully.")
    -def esl_configure(esl_link: ESL, active_link: int = 1, speed: int = 50, mode: int = 2, report: int = 0) +def esl_configure(esl_link: ESL, active_link: int = 1, speed: int = 50, mode: int = 2, report: int = 0)

    Configure the esl_link EtherSpaceWire link to the DSI.

    @@ -978,7 +738,7 @@

    Returns

    Args

    -
    esl_link : ESL
    +
    esl_link : ESL
    the ESL structure that defines the connection to the DSI
    active_link
    the port number on the DSI where the SpW link shall be activated (default=1)
    @@ -995,7 +755,7 @@

    Returns

    Expand source code -
    def esl_configure(esl_link: ESL,
    +
    def esl_configure(esl_link: 'ESL',
                       active_link: int = 1, speed: int = 50, mode: int = constants.ESL_LINK_MODE_NORMAL,
                       report: int = 0):
         """
    @@ -1021,14 +781,23 @@ 

    Returns

    Nothing """ status = esl_set_active_link(esl_link, active_link) + logger.info(f"esl_set_active_link({active_link}) -> {status = }") status = esl_set_speed(esl_link, speed) - status = esl_set_mode(esl_link, mode) + logger.info(f"esl_set_speed({speed}) -> {status = }") + if report: - status = esl_er_enable_reporting(esl_link, report)
    + status = esl_er_enable_reporting(esl_link, report) + logger.info(f"esl_er_enable_reporting(0b{report:b}) -> {status = }") + + status = esl_set_mode(esl_link, mode) + logger.info(f"esl_set_mode({esl_link_mode[mode]}={mode}) -> {status = }") + + status = esl_flush(esl_link) + logger.info(f"esl_flush() -> {status = }")
    -def esl_connection(dsi_address: str) ‑> ESL +def esl_connection(dsi_address: str) ‑> ESL

    Context Manager that opens a EtherSpaceLink connection with the DSI (Diagnostic SpaceWire Interface).

    @@ -1044,7 +813,7 @@

    Returns

    Expand source code
    @contextmanager
    -def esl_connection(dsi_address: str) -> ESL:
    +def esl_connection(dsi_address: str) -> 'ESL':
         """
         Context Manager that opens a EtherSpaceLink connection with the DSI (Diagnostic SpaceWire Interface).
     
    @@ -1073,7 +842,7 @@ 

    Returns

    Expand source code
    def esl_er_enable_reporting(esl_link, flags):
    -    result = libesl_er_enable_reporting(esl_link, flags)
    +    result = egse.dsi._libesl.libesl_er_enable_reporting(esl_link, flags)
         if result:
             raise ESLError(
                 f"Could not enable error reporting, "
    @@ -1089,7 +858,7 @@ 

    Returns

    Flush all outstanding data to the destination. This function puts queued data onto the wire.

    Args

    -
    esl_link : ESL
    +
    esl_link : ESL
    the ESL structure that defines the connection to the DSI

    Returns

    @@ -1108,7 +877,7 @@

    Returns

    Returns: 0 on success, !0 otherwise """ - result = libesl_flush(esl_link) + result = egse.dsi._libesl.libesl_flush(esl_link) # We don't want this to raise an exception, the result value should be checked by the caller instead. # if result: # raise ESLError( @@ -1118,6 +887,47 @@

    Returns

    return result
    + +
    +
    +
    + +Expand source code + +
    def esl_get_active_link(esl_link):
    +    return esl_link.contents.current_port
    +
    +
    +
    +def esl_get_esl_error(esl_link) +
    +
    +
    +
    + +Expand source code + +
    def esl_get_esl_error(esl_link):
    +    return egse.dsi._libesl.libesl_get_esl_error(esl_link)
    +
    +
    +
    +def esl_get_hwa(esl_link) +
    +
    +
    +
    + +Expand source code + +
    def esl_get_hwa(esl_link):
    +    hwa = b'012345'  # Pre-allocate the character buffer
    +    egse.dsi._libesl.libesl_get_hwa(esl_link, hwa)
    +    return hwa
    +
    +
    def esl_get_manufacturer_string(esl_link)
    @@ -1128,7 +938,20 @@

    Returns

    Expand source code
    def esl_get_manufacturer_string(esl_link):
    -    return libesl_get_manufacturer_string(esl_link).decode()
    + return egse.dsi._libesl.libesl_get_manufacturer_string(esl_link).decode()
    + + + +
    +
    +
    + +Expand source code + +
    def esl_get_number_of_links(esl_link):
    +    return egse.dsi._libesl.libesl_get_number_of_links(esl_link)
    @@ -1141,11 +964,11 @@

    Returns

    Expand source code
    def esl_get_product_string(esl_link):
    -    return libesl_get_product_string(esl_link).decode()
    + return egse.dsi._libesl.libesl_get_product_string(esl_link).decode()
    -def esl_get_receive_speed(esl_link: ESL) ‑> int +def esl_get_receive_speed(esl_link: ESL) ‑> int

    Gets the receive speed of the currently active link.

    @@ -1153,16 +976,16 @@

    Returns

    should be used instead.

    Args

    -
    esl_link : ESL
    +
    esl_link : ESL
    the ESL structure that defines the connection to the DSI

    Returns

    -

    the speed of the active link in Mbtis/s. In case of an error a value < 0 will be returned.

    +

    the speed of the active link in Mbits/s. In case of an error a value < 0 will be returned.

    Expand source code -
    def esl_get_receive_speed(esl_link: ESL) -> int:
    +
    def esl_get_receive_speed(esl_link: 'ESL') -> int:
         """
         Gets the receive speed of the currently active link.
     
    @@ -1173,9 +996,9 @@ 

    Returns

    esl_link (ESL): the ESL structure that defines the connection to the DSI Returns: - the speed of the active link in Mbtis/s. In case of an error a value < 0 will be returned. + the speed of the active link in Mbits/s. In case of an error a value < 0 will be returned. """ - return libesl_get_receive_speed(esl_link)
    + return egse.dsi._libesl.libesl_get_receive_speed(esl_link)
    @@ -1191,8 +1014,35 @@

    Returns

    return esl_link.contents.rx_timeout
    +
    +def esl_get_serial_number(esl_link) +
    +
    +
    +
    + +Expand source code + +
    def esl_get_serial_number(esl_link):
    +    hwa = esl_get_hwa(esl_link)
    +    return egse.dsi._libesl.libesl_hwa_to_serial_number_string(hwa)
    +
    +
    + +
    +
    +
    + +Expand source code + +
    def esl_is_link_connected(esl_link):
    +    return egse.dsi._libesl.libesl_link_connected(esl_link)
    +
    +
    -def esl_open_connection(dsi_address: str) ‑> ESL +def esl_open_connection(dsi_address: str) ‑> ESL

    Open a connection to the EtherSpaceLink DSI on the given IP address. @@ -1209,7 +1059,7 @@

    Returns

    Expand source code -
    def esl_open_connection(dsi_address: str) -> ESL:
    +
    def esl_open_connection(dsi_address: str) -> 'ESL':
         """
         Open a connection to the EtherSpaceLink DSI on the given IP address.
         This function will keep trying to connect for 10 seconds before aborting
    @@ -1228,13 +1078,14 @@ 

    Returns

    esl_link = None while retry: - esl_link = libesl_open(c_char_p(dsi_address.encode())) + esl_link = egse.dsi._libesl.libesl_open(c_char_p(dsi_address.encode())) if esl_link: break else: time.sleep(0.5) # wait half a second before trying again logger.info(f"Trying to connect to {dsi_address}, {retry / 2.0} sec before shutdown.") retry -= 1 + if not esl_link: raise ESLError(f"Couldn't open connection to DSI on {dsi_address}.") @@ -1244,13 +1095,13 @@

    Returns

    -def esl_print_info(esl_link: ESL) ‑> None +def esl_print_info(esl_link: ESL)

    Prints information about the connected device to the console.

    Args

    -
    esl_link : ESL
    +
    esl_link : ESL
    the ESL structure that defines the connection to the DSI

    Returns

    @@ -1259,7 +1110,7 @@

    Returns

    Expand source code -
    def esl_print_info(esl_link: ESL) -> None:
    +
    def esl_print_info(esl_link: 'ESL') -> None:
         """
         Prints information about the connected device to the console.
     
    @@ -1269,16 +1120,12 @@ 

    Returns

    Returns: nothing """ - print("Manufacturer {}".format(esl_get_manufacturer_string(esl_link))) - print("Product {}".format(esl_get_product_string(esl_link))) - print("Number of links {}".format(esl_get_number_of_links(esl_link))) - - # Pre-allocate the character buffer - - hwa = b'012345' + print(f"Manufacturer {esl_get_manufacturer_string(esl_link)}") + print(f"Product {esl_get_product_string(esl_link)}") + print(f"Number of links {esl_get_number_of_links(esl_link)} [active port={esl_get_active_link(esl_link)}]") - esl_get_hwa(esl_link, hwa) - serial_number = esl_hwa_to_serial_number_string(hwa) + hwa = esl_get_hwa(esl_link) + serial_number = esl_get_serial_number(esl_link) print(f"Serial number {serial_number}") print(f"Hardware Address 0x{hwa[0]:02X}-{hwa[1]:02X}-{hwa[2]:02X}-{hwa[3]:02X}-{hwa[4]:02X}-{hwa[5]:02X}")
    @@ -1312,7 +1159,7 @@

    Returns

    -def esl_read_packet(esl_link: ESL, timeout: int = None) ‑> Tuple[int, bytes] +def esl_read_packet(esl_link: ESL, timeout: int = None) ‑> Tuple[int, bytes]

    Reads a full packet from the SpaceWire link.

    @@ -1321,7 +1168,7 @@

    Returns

    Args

    -
    esl_link : ESL
    +
    esl_link : ESL
    the ESL structure that defines the connection to the DSI
    timeout : int
    the maximum timeout that read_packet() will wait for data before returning [milliseconds]
    @@ -1334,7 +1181,7 @@

    Returns

    Expand source code -
    def esl_read_packet(esl_link: ESL, timeout: int = None) -> Tuple[int, bytes]:
    +
    def esl_read_packet(esl_link: 'ESL', timeout: int = None) -> Tuple[int, bytes]:
         """
         Reads a full packet from the SpaceWire link.
     
    @@ -1357,7 +1204,7 @@ 

    Returns

    esl_link, rx_buffer, dsi_settings.RX_BUFFER_LENGTH, terminator_p, constants.ESL_RETURN_EXTENSION_DATA | constants.ESL_RETURN_SPECIAL_DATA, timeout=timeout) - logger.log(0, f"Number of bytes received: {bytes_received}") + # logger.debug(f"Number of bytes received: {bytes_received}") if bytes_received < 0: return bytes_received, bytes() @@ -1378,12 +1225,12 @@

    Returns

    if timeout: saved_timeout = esl_get_rx_timeout(esl_link) - esl_set_rx_timeout(esl_link, timeout) + egse.dsi._libesl.libesl_set_rx_timeout(esl_link, timeout) - result = libesl_read_packet_full(esl_link, buffer, buffer_length, rx_terminator, special_data_action) + result = egse.dsi._libesl.libesl_read_packet_full(esl_link, buffer, buffer_length, rx_terminator, special_data_action) if timeout: - esl_set_rx_timeout(esl_link, saved_timeout) + egse.dsi._libesl.libesl_set_rx_timeout(esl_link, saved_timeout) # This error handling is (or should be) done in the calling application, see for example egse.feesim.py # if result == -1: @@ -1396,14 +1243,14 @@

    Returns

    -def esl_send_timecode(esl_link: ESL, timecode: int) ‑> int +def esl_send_timecode(esl_link: ESL, timecode: int) ‑> int

    Send a timecode over the SpaceWire link.

    The 8-bit timecode argument contains six-bit of system time (time-field) and two control flags.

    Args

    -
    esl_link : ESL
    +
    esl_link : ESL
    the ESL structure that defines the connection to the DSI
    timecode : int
    an 8-bit timecode field
    @@ -1414,7 +1261,7 @@

    Returns

    Expand source code -
    def esl_send_timecode(esl_link: ESL, timecode: int) -> int:
    +
    def esl_send_timecode(esl_link: 'ESL', timecode: int) -> int:
         """
         Send a timecode over the SpaceWire link.
     
    @@ -1428,7 +1275,7 @@ 

    Returns

    0 if the request has been queued, not 0 if not. """ - result = libesl_send_timecode(esl_link, timecode) + result = egse.dsi._libesl.libesl_send_timecode(esl_link, timecode) if result: raise ESLError( @@ -1436,11 +1283,24 @@

    Returns

    f"[{esl_link.contents.ESL_error}]" ) - libesl_flush(esl_link) + result = egse.dsi._libesl.libesl_flush(esl_link) return result
    + +
    +
    +
    + +Expand source code + +
    def esl_set_active_link(esl_link, active_link):
    +    return egse.dsi._libesl.libesl_set_active_link(esl_link, active_link)
    +
    +
    def esl_set_log_file(esl_link, filename)
    @@ -1451,14 +1311,14 @@

    Returns

    Expand source code
    def esl_set_log_file(esl_link, filename):
    -    result = libesl_set_log_file(esl_link, c_char_p(filename.encode()))
    +    result = egse.dsi._libesl.libesl_set_log_file(esl_link, c_char_p(filename.encode()))
         if result:
    -        raise ESLError(f"Could not write to or open record file {filename}.")
    +        raise ESLError(f"Could not write to or open log file {filename}.")
         return result
    -def esl_set_mode(esl_link: ESL, mode: int) ‑> int +def esl_set_mode(esl_link: ESL, mode: int) ‑> int

    Set the operating mode of the currently active SpaceWire link.

    @@ -1467,7 +1327,7 @@

    Returns

    be transferred.

    Args

    -
    esl_link : ESL
    +
    esl_link : ESL
    the ESL structure that defines the connection to the DSI
    mode : int
    the link mode [DISABLED, NORMAL, LEGACY or MASTER]
    @@ -1478,7 +1338,7 @@

    Returns

    Expand source code -
    def esl_set_mode(esl_link: ESL, mode: int) -> int:
    +
    def esl_set_mode(esl_link: 'ESL', mode: int) -> int:
         """
         Set the operating mode of the currently active SpaceWire link.
     
    @@ -1494,11 +1354,10 @@ 

    Returns

    0 if the request has been queued, not 0 if not. """ - result = libesl_set_mode(esl_link, mode) + result = egse.dsi._libesl.libesl_set_mode(esl_link, mode) if result: - raise ESLError( - "Could not set mode, ESL error code={} [{}]".format(esl_error_codes[esl_link.contents.ESL_error], - esl_link.contents.ESL_error)) + raise ESLError(f"Could not set mode {esl_link_mode[mode]}.") + return result
    @@ -1512,12 +1371,25 @@

    Returns

    Expand source code
    def esl_set_record_file(esl_link, filename):
    -    result = libesl_set_record_file(esl_link, c_char_p(filename.encode()))
    +    result = egse.dsi._libesl.libesl_set_record_file(esl_link, c_char_p(filename.encode()))
         if result:
    -        raise ESLError(f"Could not write to or open log file {filename}.")
    +        raise ESLError(f"Could not write to or open record file {filename}.")
         return result
    +
    +def esl_set_rx_timeout(esl_link, timeout: int) +
    +
    +
    +
    + +Expand source code + +
    def esl_set_rx_timeout(esl_link, timeout: int):
    +    esl_link.contents.rx_timeout = timeout
    +
    +
    def esl_set_speed(esl_link, speed)
    @@ -1528,7 +1400,7 @@

    Returns

    Expand source code
    def esl_set_speed(esl_link, speed):
    -    result = libesl_set_speed(esl_link, speed)
    +    result = egse.dsi._libesl.libesl_set_speed(esl_link, speed)
         if result:
             raise ESLError("Could not set speed to {}, ESL error code={} [{}]".format(speed, esl_error_codes[
                 esl_link.contents.ESL_error], esl_link.contents.ESL_error))
    @@ -1536,7 +1408,7 @@ 

    Returns

    -def esl_write_packet(esl_link: ESL, buffer, buffer_length: int, tx_terminator: int) ‑> int +def esl_write_packet(esl_link: ESL, buffer, buffer_length: int, tx_terminator: int) ‑> int

    Queue data for transmission over the SpaceWire cable. If there is no room left in the buffer, @@ -1545,7 +1417,7 @@

    Returns

    To guarantee transmission of this data you need to call the esl_flush() function.

    Args

    -
    esl_link : ESL
    +
    esl_link : ESL
    the ESL structure that defines the connection to the DSI
    buffer
    the data to send
    @@ -1557,13 +1429,13 @@

    Args

    Returns

    return_code
    -
    0 on success, < 0 otherwise
    +
    0 on success, < 0 when an error occurred.
    Expand source code -
    def esl_write_packet(esl_link: ESL, buffer, buffer_length: int, tx_terminator: int) -> int:
    +
    def esl_write_packet(esl_link: 'ESL', buffer, buffer_length: int, tx_terminator: int) -> int:
         """
         Queue data for transmission over the SpaceWire cable. If there is no room left in the buffer,
         the buffer is transmitted.
    @@ -1578,24 +1450,10 @@ 

    Returns

    tx_terminator: additional metadata about the frame we are transmitting (EOP, EEP, PART_EOP_EEP, EXTN) Returns: - return_code: 0 on success, < 0 otherwise + return_code: 0 on success, < 0 when an error occurred. """ - # logger.debug( - # f"Calling esl_write_packet: " - # f"buffer={pp_packet(buffer)}, buffer_length={buffer_length}, tx_terminator=0x{tx_terminator:x} " - # f"[{get_terminator_name(tx_terminator) or get_extension_name(tx_terminator)}]." - # ) - result = libesl_write_packet(esl_link, buffer, buffer_length, tx_terminator) - - # This error handling is (or should be) done in the calling application, see for example egse.feesim.py - # if result == -1: - # raise ESLError( - # f"Could not write the packet, " - # f"ESL error code = {esl_error_codes[esl_link.contents.ESL_error]} [{esl_link.contents.ESL_error}]" - # ) - - # logger.debug(f"Returning from esl_write_packet: buffer={pp_packet(buffer)}, result = {result}.") + result = egse.dsi._libesl.libesl_write_packet(esl_link, buffer, buffer_length, tx_terminator) return result
    @@ -1745,73 +1603,6 @@

    Returns

    return msg
    -
    -def pretty_print_packet(packet) ‑> str -
    -
    -
    -
    - -Expand source code - -
    def pretty_print_packet(packet) -> str:
    -
    -    from egse.fee import is_hk_data_packet
    -
    -    msg = f"packet is of type {type(packet)}, with length {len(packet)}\n"
    -
    -    # When the packet is of type ctypes.c_char_Array_24 (i.e. ctypes array of c_char)
    -    # then convert the packet into a bytes object.
    -
    -    if hasattr(packet, 'raw'):
    -        packet = packet.raw
    -
    -    # First check if this is a timecode packet because this is only 2 bytes long and reading the instruction field
    -    # will result in an IndexError otherwise.
    -
    -    if is_timecode(packet):
    -        msg += (
    -            f"Time code received: 0x{packet[1]:02x}\n"
    -        )
    -    elif get_protocol_id(packet) == RMAP_PROTOCOL_ID:
    -        import egse.dsi.rmap
    -        instruction_field = egse.dsi.rmap.get_instruction_field(packet)
    -        if instruction_field == 0x4C:
    -            msg += pretty_print_read_request_packet(packet)
    -        elif instruction_field == 0x4C & 0x3F:
    -            msg += pretty_print_read_request_reply_packet(packet)
    -        elif instruction_field == 0x7C:
    -            msg += pretty_print_verified_write_request_packet(packet)
    -        elif instruction_field == 0x6C:
    -            msg += pretty_print_unverified_write_request_packet(packet)
    -        elif instruction_field == 0x3C & 0x3F:
    -            msg += pretty_print_write_request_reply_packet(packet)
    -        else:
    -            msg += (
    -                f"RMAP packet (to-be-implemented)\n"
    -                f'{pp_packet(packet)}'
    -            )
    -    elif get_protocol_id(packet) == CCSDS_PROTOCOL_ID:
    -        msg += (
    -            f"CCSDS Packet\n"
    -            f"Packet: {' '.join([hex(x) for x in packet[:min(80, len(packet))]])} [max. 80 bytes printed]."
    -        )
    -    elif get_protocol_id(packet) == DATA_HK_PROTOCOL_ID and is_hk_data_packet(packet):
    -        # FIXME: this puts an unnecessary dependency on the esl.py module. The HousekeepingPacket is PLATO specific
    -        #        and should not be in the esl.py module
    -        msg += (
    -            f"HK Packet:\n"
    -            f"{HousekeepingPacket(packet)}"
    -        )
    -    else:
    -        msg += (
    -            f"Extended Protocol Identifier is not supported.\n"
    -            f"Packet: {' '.join([hex(x) for x in packet[:min(80, len(packet))]])} [max. 80 bytes printed]."
    -        )
    -
    -    return msg
    -
    -
    def pretty_print_read_request_packet(packet)
    @@ -1952,327 +1743,6 @@

    Returns

    Classes

    -
    -class ESL -(*args, **kwargs) -
    -
    -

    Structure base class

    -
    - -Expand source code - -
    class ESL(Structure):
    -    _fields_ = [
    -        ("sock", c_int),
    -        ("tx_buffer", c_char_p),
    -        ("tx_buffer_length", c_size_t),
    -        ("tx_buffer_content", c_size_t),
    -        ("rx_buffer", c_char_p),
    -        ("rx_buffer_length", c_size_t),
    -        ("rx_buffer_content", c_ssize_t),
    -        ("rx_buffer_ptr", c_int),
    -        ("rx_state", c_int),
    -        ("rx_count", c_int),
    -        ("rx_param", c_int),
    -        ("rx_timeout", c_int),
    -        ("rx_final_flags", c_int),
    -        ("extn_count", c_int),
    -        ("extn_size", c_int),
    -        ("extn_cmd", c_int),
    -        ("extn_byte", c_int),
    -        ("extn_link", c_int),
    -        ("cmd_buffer", c_char_p),
    -        ("time_buffer", c_char_p),
    -
    -        ("special_cb", c_void_p),
    -        ("extension_cb", c_void_p),
    -
    -        ("number_of_links", c_int),
    -        ("number_of_slots", c_int),
    -        ("slot_content", c_int_p),
    -        ("record_file", c_void_p),
    -        ("log_file", c_void_p),
    -        ("max_length", c_int),
    -        ("log_rx_port", c_int),
    -        ("log_tx_port", c_int),
    -
    -        ("ram_io", c_int),
    -        ("current_port", c_int),
    -
    -        ("check_record_writes", c_int),
    -        ("total_raw_bytes_received", c_uint64),
    -
    -        ("options", c_char_p),
    -        ("option", c_int),
    -        ("ESL_error_errno", c_int),
    -
    -        ("rx_timeout_returns_error", c_int),
    -
    -        ("max_dumps", c_uint32),
    -        ("context", c_void_p),
    -
    -        ("rxinfo", c_uint32),
    -
    -        ("read_calls", c_uint64),
    -        ("read_io_calls", c_uint64),
    -        ("file", c_int),
    -        ("filesize", c_uint64),
    -        ("recordsize", c_uint64),
    -
    -        ("ram_io_int", c_int),
    -
    -        ("eintr_enabled", c_int),
    -        ("timezero", c_size_t),
    -        ("timedata", c_uint64),
    -        ("timedata_ns", c_uint32),
    -
    -        ("partialframesz", c_size_t),
    -        ("partialframe_offset", c_size_t),
    -        ("system_type", c_int),
    -        ("auto_flush", c_int),
    -        ("live", c_int),
    -
    -        ("epollfd", c_int),
    -    ]
    -
    -

    Ancestors

    -
      -
    • _ctypes.Structure
    • -
    • _ctypes._CData
    • -
    -

    Instance variables

    -
    -
    var ESL_error_errno
    -
    -

    Structure/Union member

    -
    -
    var auto_flush
    -
    -

    Structure/Union member

    -
    -
    var check_record_writes
    -
    -

    Structure/Union member

    -
    -
    var cmd_buffer
    -
    -

    Structure/Union member

    -
    -
    var context
    -
    -

    Structure/Union member

    -
    -
    var current_port
    -
    -

    Structure/Union member

    -
    -
    var eintr_enabled
    -
    -

    Structure/Union member

    -
    -
    var epollfd
    -
    -

    Structure/Union member

    -
    -
    var extension_cb
    -
    -

    Structure/Union member

    -
    -
    var extn_byte
    -
    -

    Structure/Union member

    -
    -
    var extn_cmd
    -
    -

    Structure/Union member

    -
    -
    var extn_count
    -
    -

    Structure/Union member

    -
    - -
    -

    Structure/Union member

    -
    -
    var extn_size
    -
    -

    Structure/Union member

    -
    -
    var file
    -
    -

    Structure/Union member

    -
    -
    var filesize
    -
    -

    Structure/Union member

    -
    -
    var live
    -
    -

    Structure/Union member

    -
    -
    var log_file
    -
    -

    Structure/Union member

    -
    -
    var log_rx_port
    -
    -

    Structure/Union member

    -
    -
    var log_tx_port
    -
    -

    Structure/Union member

    -
    -
    var max_dumps
    -
    -

    Structure/Union member

    -
    -
    var max_length
    -
    -

    Structure/Union member

    -
    - -
    -

    Structure/Union member

    -
    -
    var number_of_slots
    -
    -

    Structure/Union member

    -
    -
    var option
    -
    -

    Structure/Union member

    -
    -
    var options
    -
    -

    Structure/Union member

    -
    -
    var partialframe_offset
    -
    -

    Structure/Union member

    -
    -
    var partialframesz
    -
    -

    Structure/Union member

    -
    -
    var ram_io
    -
    -

    Structure/Union member

    -
    -
    var ram_io_int
    -
    -

    Structure/Union member

    -
    -
    var read_calls
    -
    -

    Structure/Union member

    -
    -
    var read_io_calls
    -
    -

    Structure/Union member

    -
    -
    var record_file
    -
    -

    Structure/Union member

    -
    -
    var recordsize
    -
    -

    Structure/Union member

    -
    -
    var rx_buffer
    -
    -

    Structure/Union member

    -
    -
    var rx_buffer_content
    -
    -

    Structure/Union member

    -
    -
    var rx_buffer_length
    -
    -

    Structure/Union member

    -
    -
    var rx_buffer_ptr
    -
    -

    Structure/Union member

    -
    -
    var rx_count
    -
    -

    Structure/Union member

    -
    -
    var rx_final_flags
    -
    -

    Structure/Union member

    -
    -
    var rx_param
    -
    -

    Structure/Union member

    -
    -
    var rx_state
    -
    -

    Structure/Union member

    -
    -
    var rx_timeout
    -
    -

    Structure/Union member

    -
    -
    var rx_timeout_returns_error
    -
    -

    Structure/Union member

    -
    -
    var rxinfo
    -
    -

    Structure/Union member

    -
    -
    var slot_content
    -
    -

    Structure/Union member

    -
    -
    var sock
    -
    -

    Structure/Union member

    -
    -
    var special_cb
    -
    -

    Structure/Union member

    -
    -
    var system_type
    -
    -

    Structure/Union member

    -
    -
    var time_buffer
    -
    -

    Structure/Union member

    -
    -
    var timedata
    -
    -

    Structure/Union member

    -
    -
    var timedata_ns
    -
    -

    Structure/Union member

    -
    -
    var timezero
    -
    -

    Structure/Union member

    -
    -
    var total_raw_bytes_received
    -
    -

    Structure/Union member

    -
    -
    var tx_buffer
    -
    -

    Structure/Union member

    -
    -
    var tx_buffer_content
    -
    -

    Structure/Union member

    -
    -
    var tx_buffer_length
    -
    -

    Structure/Union member

    -
    -
    -
    class ESLError (*args, **kwargs) @@ -2292,18 +1762,6 @@

    Ancestors

  • builtins.BaseException
  • -
    -class esl_p -(*args, **kwargs) -
    -
    -

    XXX to be provided

    -

    Ancestors

    -
      -
    • _ctypes._Pointer
    • -
    • _ctypes._CData
    • -
    -
    @@ -2325,19 +1783,27 @@

    Index

  • esl_connection
  • esl_er_enable_reporting
  • esl_flush
  • +
  • esl_get_active_link
  • +
  • esl_get_esl_error
  • +
  • esl_get_hwa
  • esl_get_manufacturer_string
  • +
  • esl_get_number_of_links
  • esl_get_product_string
  • esl_get_receive_speed
  • esl_get_rx_timeout
  • +
  • esl_get_serial_number
  • +
  • esl_is_link_connected
  • esl_open_connection
  • esl_print_info
  • esl_print_summary_of_structure
  • esl_read_packet
  • esl_read_packet_full
  • esl_send_timecode
  • +
  • esl_set_active_link
  • esl_set_log_file
  • esl_set_mode
  • esl_set_record_file
  • +
  • esl_set_rx_timeout
  • esl_set_speed
  • esl_write_packet
  • get_extension_name
  • @@ -2347,7 +1813,6 @@

    Index

  • is_terminator_code
  • is_timecode
  • pp_packet
  • -
  • pretty_print_packet
  • pretty_print_read_request_packet
  • pretty_print_read_request_reply_packet
  • pretty_print_unverified_write_request_packet
  • @@ -2358,73 +1823,8 @@

    Index

  • Classes

  • diff --git a/docs/api/egse/dsi/index.html b/docs/api/egse/dsi/index.html index 0dcadfa..9e73cba 100644 --- a/docs/api/egse/dsi/index.html +++ b/docs/api/egse/dsi/index.html @@ -19,9 +19,47 @@
    -

    Namespace egse.dsi

    +

    Module egse.dsi

    +
    + +Expand source code + +
    # The purpose of this code is to lazy load the ESL shared library and the functions loaded from that library.
    +# The reason for doing this is (1) remove unnecessary imports when not needed by the app, and (2) allow code
    +# to run on a different platform that cannot load the shared library, but still needs some functionality from
    +# the `egse.dsi` module.
    +
    +import logging
    +
    +__all__ = [
    +    "_libesl",
    +]
    +
    +import importlib
    +
    +_LOGGER = logging.getLogger(__name__)
    +_LAZY_LOADING_CACHE: dict = {}
    +
    +
    +def __getattr__(name):
    +
    +    try:
    +        return _LAZY_LOADING_CACHE[name]
    +    except KeyError:
    +        pass
    +
    +    if name not in __all__:
    +        raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
    +
    +    module = importlib.import_module("." + name, __name__)
    +
    +    _LOGGER.info(f"Module {module.__name__} imported.")
    +    _LAZY_LOADING_CACHE[name] = module
    +
    +    return module
    +

    Sub-modules

    @@ -38,7 +76,7 @@

    Sub-modules

    egse.dsi.rmap
    -

    This module provides Python wrapper functions to (most of) the library functions from the C library ESL-RMAP.c …

    +

    This module provides a Python interface to RMAP command packets.

    egse.dsi.rmapci
    diff --git a/docs/api/egse/alert/alert_manager_interface.html b/docs/api/egse/dsi/multiconn_client.html similarity index 53% rename from docs/api/egse/alert/alert_manager_interface.html rename to docs/api/egse/dsi/multiconn_client.html index ff7394d..711ac11 100644 --- a/docs/api/egse/alert/alert_manager_interface.html +++ b/docs/api/egse/dsi/multiconn_client.html @@ -4,7 +4,7 @@ -egse.alert.alert_manager_interface API documentation +egse.dsi.multiconn_client API documentation @@ -19,37 +19,84 @@
    -

    Module egse.alert.alert_manager_interface

    +

    Module egse.dsi.multiconn_client

    Expand source code -
    from egse.decorators import dynamic_interface
    -from egse.control import Response, Success
    +
    import sys
    +import socket
    +import selectors
    +import types
     
    -class AlertManagerInterface:
    -    @dynamic_interface
    -    def start_alert_monitor(self, configuration):
    -        NotImplemented
    -    
    -    @dynamic_interface
    -    def stop_alert_monitor(self):
    -        NotImplemented
    -        
    -    @dynamic_interface
    -    def load_configuration(self, phase):
    -        NotImplemented
    -        
    -    @dynamic_interface
    -    def device_connection_lost(self, device_code, args):
    -        NotImplemented
    -        
    -    @dynamic_interface
    -    def get_alerts(self, sensor):
    -        NotImplemented
    -        
    +sel = selectors.DefaultSelector() +messages = [b"Message 1 from client.", b"Message 2 from client."] + + +def start_connections(host, port, num_conns): + server_addr = (host, port) + for i in range(0, num_conns): + connid = i + 1 + print(f"Starting connection {connid} to {server_addr}") + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setblocking(False) + sock.connect_ex(server_addr) + events = selectors.EVENT_READ | selectors.EVENT_WRITE + data = types.SimpleNamespace( + connid=connid, + msg_total=sum(len(m) for m in messages), + recv_total=0, + messages=messages.copy(), + outb=b"", + ) + sel.register(sock, events, data=data) + + + +def service_connection(key, mask): + sock = key.fileobj + data = key.data + if mask & selectors.EVENT_READ: + recv_data = sock.recv(1024) # Should be ready to read + if recv_data: + print(f"Received {recv_data!r} from connection {data.connid}") + data.recv_total += len(recv_data) + if not recv_data or data.recv_total == data.msg_total: + print(f"Closing connection {data.connid}") + sel.unregister(sock) + sock.close() + if mask & selectors.EVENT_WRITE: + if not data.outb and data.messages: + data.outb = data.messages.pop(0) + if data.outb: + print(f"Sending {data.outb!r} to connection {data.connid}") + sent = sock.send(data.outb) # Should be ready to write + data.outb = data.outb[sent:] + + +if __name__ == "__main__": + if len(sys.argv) != 4: + print(f"Usage: {sys.argv[0]} <host> <port> <num_connections>") + sys.exit(1) + + host, port, num_conns = sys.argv[1:4] + start_connections(host, int(port), int(num_conns)) + + try: + while True: + events = sel.select(timeout=1) + if events: + for key, mask in events: + service_connection(key, mask) + # Check for a socket being monitored to continue. + if not sel.get_map(): + break + except KeyboardInterrupt: + print("Caught keyboard interrupt, exiting") + finally: + sel.close()
    @@ -57,49 +104,10 @@

    Module egse.alert.alert_manager_interface

    -
    -
    -

    Classes

    -
    -
    -class AlertManagerInterface -
    -
    -
    -
    - -Expand source code - -
    class AlertManagerInterface:
    -    @dynamic_interface
    -    def start_alert_monitor(self, configuration):
    -        NotImplemented
    -    
    -    @dynamic_interface
    -    def stop_alert_monitor(self):
    -        NotImplemented
    -        
    -    @dynamic_interface
    -    def load_configuration(self, phase):
    -        NotImplemented
    -        
    -    @dynamic_interface
    -    def device_connection_lost(self, device_code, args):
    -        NotImplemented
    -        
    -    @dynamic_interface
    -    def get_alerts(self, sensor):
    -        NotImplemented
    -
    -

    Subclasses

    - -

    Methods

    +

    Functions

    -
    -def device_connection_lost(self, device_code, args) +
    +def service_connection(key, mask)
    @@ -107,13 +115,29 @@

    Methods

    Expand source code -
    @dynamic_interface
    -def device_connection_lost(self, device_code, args):
    -    NotImplemented
    +
    def service_connection(key, mask):
    +    sock = key.fileobj
    +    data = key.data
    +    if mask & selectors.EVENT_READ:
    +        recv_data = sock.recv(1024)  # Should be ready to read
    +        if recv_data:
    +            print(f"Received {recv_data!r} from connection {data.connid}")
    +            data.recv_total += len(recv_data)
    +        if not recv_data or data.recv_total == data.msg_total:
    +            print(f"Closing connection {data.connid}")
    +            sel.unregister(sock)
    +            sock.close()
    +    if mask & selectors.EVENT_WRITE:
    +        if not data.outb and data.messages:
    +            data.outb = data.messages.pop(0)
    +        if data.outb:
    +            print(f"Sending {data.outb!r} to connection {data.connid}")
    +            sent = sock.send(data.outb)  # Should be ready to write
    +            data.outb = data.outb[sent:]
    -
    -def get_alerts(self, sensor) +
    +def start_connections(host, port, num_conns)
    @@ -121,56 +145,28 @@

    Methods

    Expand source code -
    @dynamic_interface
    -def get_alerts(self, sensor):
    -    NotImplemented
    - -
    -
    -def load_configuration(self, phase) -
    -
    -
    -
    - -Expand source code - -
    @dynamic_interface
    -def load_configuration(self, phase):
    -    NotImplemented
    -
    -
    -
    -def start_alert_monitor(self, configuration) -
    -
    -
    -
    - -Expand source code - -
    @dynamic_interface
    -def start_alert_monitor(self, configuration):
    -    NotImplemented
    -
    -
    -
    -def stop_alert_monitor(self) -
    -
    -
    -
    - -Expand source code - -
    @dynamic_interface
    -def stop_alert_monitor(self):
    -    NotImplemented
    +
    def start_connections(host, port, num_conns):
    +    server_addr = (host, port)
    +    for i in range(0, num_conns):
    +        connid = i + 1
    +        print(f"Starting connection {connid} to {server_addr}")
    +        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    +        sock.setblocking(False)
    +        sock.connect_ex(server_addr)
    +        events = selectors.EVENT_READ | selectors.EVENT_WRITE
    +        data = types.SimpleNamespace(
    +            connid=connid,
    +            msg_total=sum(len(m) for m in messages),
    +            recv_total=0,
    +            messages=messages.copy(),
    +            outb=b"",
    +        )
    +        sel.register(sock, events, data=data)
    -
    -
    +
    +
    @@ -2172,7 +837,7 @@

    Args

    the ESL structure that defines the connection to the DSI

    Returns

    -

    an RMAP link connection

    +

    an RMAP link connection.

    Expand source code @@ -2186,456 +851,87 @@

    Returns

    esl_link (ESL): the ESL structure that defines the connection to the DSI Returns: - an RMAP link connection - """ - yield rmap_open_connection(esl_link)
    -
    - -
    -def rmap_crc_check(data, start, length) -
    -
    -
    -
    - -Expand source code - -
    def rmap_crc_check(data, start, length):
    -    # logger.debug(f"Calling rmap_crc_check(data, {start}, {length})")
    -    return librmap_crc_check(data, start, length)
    -
    -
    -
    -def rmap_get_initiator_logical_address(rmap_link) -
    -
    -
    -
    - -Expand source code - -
    def rmap_get_initiator_logical_address(rmap_link):
    -    # logger.debug("Calling rmap_get_initiator_logical_address(rmap_link)")
    -    return librmap_get_initiator_logical_address(rmap_link)
    -
    -
    -
    -def rmap_get_target_key(rmap_link) -
    -
    -
    -
    - -Expand source code - -
    def rmap_get_target_key(rmap_link):
    -    logger.debug("Calling rmap_get_target_key(rmap_link)")
    -    return librmap_get_target_key(rmap_link)
    -
    -
    -
    -def rmap_get_target_logical_address(rmap_link) -
    -
    -
    -
    - -Expand source code - -
    def rmap_get_target_logical_address(rmap_link):
    -    logger.debug("Calling rmap_get_target_logical_address(rmap_link)")
    -    return librmap_get_target_logical_address(rmap_link)
    -
    -
    -
    -def rmap_open_connection(esl_link) -
    -
    -
    -
    - -Expand source code - -
    def rmap_open_connection(esl_link):
    -
    -    logger.info(f"Open and setup RMAP connection to {esl_get_product_string(esl_link)}")
    -
    -    rmap_link = librmap_open(esl_link)
    -    if not rmap_link:
    -        raise RMAPError(f"Couldn't open RMAP connection to {esl_get_product_string(esl_link)}")
    -
    -    rmap_set_verbosity(1)     # 1=normal; 5 or 15 for lots of debugging
    -
    -    logger.info("RMAP connection opened successfully.")
    -
    -    return rmap_link
    -
    -
    -
    -def rmap_read(rmap_link: ESL_RMAP, address: int, length: int, timeout: int = 1000) -
    -
    -

    Read length bytes from the remote memory starting at address. If there is no reply -after the given timeout, TODO: WHAT WILL HAPPEN THEN?

    -
    -

    Note: We are using the global rxbuf read buffer here.

    -

    The content of the buffer will be overwritten by the RMAP read request.

    -
    -

    Args

    -
    -
    rmap_link
    -
    the RMAP link connection
    -
    address
    -
    the start address (32-bit aligned) in the remote memory
    -
    length
    -
    the number of bytes to read from the remote memory
    -
    timeout
    -
    timeout in milli-seconds
    -
    -

    Returns

    -

    the buffer containing the data read from the remote memory.

    -
    - -Expand source code - -
    def rmap_read(rmap_link: ESL_RMAP, address: int, length: int, timeout: int = 1000):
    -    """
    -    Read `length` bytes from the remote memory starting at `address`. If there is no reply
    -    after the given `timeout`, TODO: WHAT WILL HAPPEN THEN?
    -
    -    .. note:: We are using the global ``rxbuf`` read buffer here.
    -        The content of the buffer will be overwritten by the RMAP read request.
    -
    -    Args:
    -        rmap_link: the RMAP link connection
    -        address: the start address (32-bit aligned) in the remote memory
    -        length: the number of bytes to read from the remote memory
    -        timeout: timeout in milli-seconds
    -
    -    Returns:
    -        the buffer containing the data read from the remote memory.
    -
    +        an RMAP link connection.
         """
    +    rmap_link = rmap_open_connection(esl_link)
    +    yield rmap_link
     
    -    data_length = librmap_read_TO(rmap_link, address, rxbuf, length, status_p, timeout)
    -
    -    # If data_length < 0 it can have the following values:
    -    # -1 = status was != 0 indicating a read error, the packet was eaten...discarded?
    -    # -4 = wrong TLA field in header
    -    # -5 = wrong TID field in header
    -
    -    # FIXME: Think about if we should raise an RMAPError here instead of returning None when result is negative.
    -    #        I would go for an Exception, since we loose the information on the error anyway as code information (
    -    #        None is returned, not data_length), and now the caller must check the return code from this read command.
    -
    -    if data_length < 0:
    -        logger.warning(
    -            f"Couldn't read data within timeout of {timeout} ms, "
    -            f"ESL RMAP Error Code = {esl_rmap_error_codes[rmap_link.contents.ESL_RMAP_error]} "
    -            f"[{rmap_link.contents.ESL_RMAP_error}]"
    -        )
    -        return None
    -    else:
    -        return rxbuf[:data_length]
    + rmap_close_connection(rmap_link)
    -
    -def rmap_read_request(rmap_link: ESL_RMAP, address: int, length: int, timeout: int = 1000) ‑> Tuple[int, bytes] +
    +def rmap_initialise(esl_link) ‑> ESL_RMAP
    -

    Read length bytes from the remote memory starting at address.

    -
    -

    TODO

    -

    the timeout parameter is currently not implemented

    -
    -

    Args

    -
    -
    rmap_link
    -
    the RMAP link connection
    -
    address
    -
    the start address (32-bit aligned) in the remote memory
    -
    length
    -
    the number of bytes to read from the remote memory
    -
    timeout
    -
    timeout in milli-seconds
    -
    -

    Returns

    -

    A tuple containing the terminator value and the RMAP Reply packet with the data read from the remote memory.

    +

    Initialise the RMAP structure with predefined settings for PLATO.

    Expand source code -
    def rmap_read_request(rmap_link: ESL_RMAP, address: int, length: int, timeout: int = 1000) -> Tuple[int, bytes]:
    -    """
    -    Read `length` bytes from the remote memory starting at `address`.
    -
    -    .. todo:: the timeout parameter is currently not implemented
    -
    -    Args:
    -        rmap_link: the RMAP link connection
    -        address: the start address (32-bit aligned) in the remote memory
    -        length: the number of bytes to read from the remote memory
    -        timeout: timeout in milli-seconds
    -
    -    Returns:
    -        A tuple containing the terminator value and the RMAP Reply packet with the data read from the remote memory.
    -
    +
    def rmap_initialise(esl_link) -> ESL_RMAP:
         """
    -    buffer = create_rmap_read_request_packet(rmap_link, address, length)
    -
    -    logger.log(5, "Pretty Print Read Request Packet:\n" + pretty_print_packet(buffer))
    -
    -    result = esl_write_packet(rmap_link.contents.spw_device, buffer, len(buffer), constants.ESL_EOP)
    -    if result:
    -        raise RMAPError(
    -            f"Couldn't send data within timeout of {timeout} ms, "
    -            f"ESL RMAP Error Code = {esl_rmap_error_codes[rmap_link.contents.ESL_RMAP_error]} ["
    -            f"{rmap_link.contents.ESL_RMAP_error}]")
    -
    -    result = esl_flush(rmap_link.contents.spw_device)
    -    if result:
    -        raise RMAPError(
    -            f"Couldn't send data or clear buffers, "
    -            f"ESL RMAP Error Code = {esl_rmap_error_codes[rmap_link.contents.ESL_RMAP_error]} ["
    -            f"{rmap_link.contents.ESL_RMAP_error}]")
    -
    -    # Read the read request reply
    -
    -    terminator, rx_buffer = esl_read_packet(rmap_link.contents.spw_device)
    -
    -    logger.log(5, "Pretty Print Read Request Reply Packet:\n" + pretty_print_packet(rx_buffer))
    -
    -    return terminator, rx_buffer
    -
    -
    -
    -def rmap_set_initiator_logical_address(rmap_link, address) -
    -
    -
    -
    - -Expand source code - -
    def rmap_set_initiator_logical_address(rmap_link, address):
    -    # logger.debug(f"Calling rmap_set_initiator_logical_address(rmap_link, 0x{address:02X})")
    -    librmap_set_initiator_logical_address(rmap_link, address)
    -
    -
    -
    -def rmap_set_target_key(rmap_link, key) -
    -
    -
    -
    - -Expand source code - -
    def rmap_set_target_key(rmap_link, key):
    -    logger.debug(f"Calling rmap_set_target_key({key})")
    -    librmap_set_target_key(rmap_link, key)
    -
    -
    -
    -def rmap_set_target_logical_address(rmap_link, address) -
    -
    -
    -
    - -Expand source code - -
    def rmap_set_target_logical_address(rmap_link, address):
    -    logger.debug(f"Calling rmap_set_target_logical_address(rmap_link, 0x{address:02X})")
    -    librmap_set_target_logical_address(rmap_link, address)
    -
    -
    -
    -def rmap_set_target_spw_address(rmap_link, spw_address, spw_address_length) -
    -
    -
    -
    - -Expand source code - -
    def rmap_set_target_spw_address(rmap_link, spw_address, spw_address_length):
    -    logger.debug(f"Calling rmap_set_target_spw_address(rmap_link, spw_address, {spw_address_length})")
    -    librmap_set_target_spw_address(rmap_link, spw_address, spw_address_length)
    -
    -
    -
    -def rmap_set_verbosity(flags) -
    -
    -

    Set vebosity of the RMAP API.

    -

    Report errors by default:

    -
      -
    • bitval -1 : output textual error messages
    • -
    • bitval -2 : output SpaceWire read/write packet tracing
    • -
    • bitval -4 : output API function call tracing
    • -
    • bitval -8 : output API parameter -/ data packet tracing
    • -
    • bitval 16 : output API data packet tracing
    • -
    -

    Args

    -
    -
    flags : int
    -
    verbosy level
    -
    -

    Returns

    -

    None

    -
    - -Expand source code - -
    def rmap_set_verbosity(flags):
    +    Initialise the RMAP structure with predefined settings for PLATO.
         """
    -    Set vebosity of the RMAP API.
    -
    -    Report errors by default:
    +    esl_rmap: ESL_RMAP = ESL_RMAP()
     
    -    * bitval  1 : output textual error messages
    -    * bitval  2 : output SpaceWire read/write packet tracing
    -    * bitval  4 : output API function call tracing
    -    * bitval  8 : output API parameter  / data packet tracing
    -    * bitval 16 : output API data packet tracing
    -
    -    Args:
    -        flags (int): verbosy level
    -
    -    Returns:
    -        None
    +    esl_rmap.spw_device = esl_link
    +    esl_rmap.target_key = 0x00
    +    esl_rmap.target_logical_address = 0xFE
    +    esl_rmap.target_spw_address = 0x00
    +    esl_rmap.target_spw_address_len = 0
    +    esl_rmap.reply_spw_address = 0x0
    +    esl_rmap.reply_spw_address_len = 0
    +    esl_rmap.initiator_logical_address = 0xFE
    +    esl_rmap.transaction_identifier = 99
    +    esl_rmap.ESL_RMAP_error = 99
     
    -    """
    -    librmap_set_verbosity(flags)
    + return esl_rmap
    -
    -def rmap_write_request(rmap_link: ESL_RMAP, address: int, data: bytes, length: int = 4, timeout: int = 1000) ‑> Tuple[int, bytes] +
    +def rmap_open_connection(esl_link)
    -

    Sends an RMAP write command over the SpaceWire link.

    -

    Based on the address this function will decided to send a verified or unverified write request.

    -
    -

    TODO

    -

    the timeout parameter is currently not implemented

    -
    +

    Initialises and opens an RMAP connection on an EtherSpaceLink (ESL) connection.

    +

    This requires a valid link to an ESL connection which shall be created with the following +command:

    +
    esl_link = esl_open_connection(<DSI IP Address>)
    +

    Args

    -
    rmap_link : ESL_RMAP
    -
    the RMAP link connection
    -
    address
    -
    the starting memory address to which the data from buffer will be written
    -
    data
    -
    the data that will be written into the targets memory
    -
    length
    -
    the number of bytes to write (the buffer maybe longer) [default=4]
    -
    timeout
    -
    timeout in milliseconds [default=1000]
    +
    esl_link
    +
    a connection link to the DSI SpaceWire interface

    Returns

    -
    -
    return_code
    -
    zero (0) on success.
    -
    -

    Raises

    -
    -
    RMAPError
    -
    when data can not be written on the target.
    -
    +

    The RMAP connection link.

    Expand source code -
    def rmap_write_request(rmap_link: ESL_RMAP,
    -                       address: int, data: bytes, length: int = 4, timeout: int = 1000) -> Tuple[int, bytes]:
    +
    def rmap_open_connection(esl_link):
         """
    -    Sends an RMAP write command over the SpaceWire link.
    +    Initialises and opens an RMAP connection on an EtherSpaceLink (ESL) connection.
     
    -    Based on the address this function will decided to send a verified or unverified write request.
    +    This requires a valid link to an ESL connection which shall be created with the following
    +    command:
     
    -    .. todo:: the timeout parameter is currently not implemented
    +        esl_link = esl_open_connection(<DSI IP Address>)
     
         Args:
    -        rmap_link (ESL_RMAP): the RMAP link connection
    -        address: the starting memory address to which the data from buffer will be written
    -        data: the data that will be written into the targets memory
    -        length: the number of bytes to write (the buffer maybe longer) [default=4]
    -        timeout: timeout in milliseconds [default=1000]
    +        esl_link: a connection link to the DSI SpaceWire interface
     
         Returns:
    -        return_code: zero (0) on success.
    -
    -    Raises:
    -        RMAPError: when data can not be written on the target.
    +        The RMAP connection link.
     
         """
     
    -    if CRITICAL_AREA_START <= address <= CRITICAL_AREA_END:
    -        buffer = create_rmap_verified_write_packet(rmap_link, address, data)
    -    else:
    -        buffer = create_rmap_unverified_write_packet(rmap_link, address, data, length)
    -
    -    logger.log(5, "Pretty Print Write Request Packet:\n" + pretty_print_packet(buffer))
    -
    -    result = esl_write_packet(rmap_link.contents.spw_device, buffer, len(buffer), constants.ESL_EOP)
    -    if result:
    -        raise RMAPError(
    -            f"Couldn't send data within timeout of {timeout} ms, "
    -            f"ESL RMAP Error Code = {esl_rmap_error_codes[rmap_link.contents.ESL_RMAP_error]} ["
    -            f"{rmap_link.contents.ESL_RMAP_error}]")
    -
    -    result = esl_flush(rmap_link.contents.spw_device)
    -    if result:
    -        raise RMAPError(
    -            f"Couldn't send data or clear buffers, "
    -            f"ESL RMAP Error Code = {esl_rmap_error_codes[rmap_link.contents.ESL_RMAP_error]} ["
    -            f"{rmap_link.contents.ESL_RMAP_error}]")
    -
    -    # Read the write reply
    +    LOGGER.info(f"Open and setup RMAP connection to {esl_get_product_string(esl_link)}")
     
    -    terminator, rx_buffer = esl_read_packet(rmap_link.contents.spw_device)
    +    rmap_link = rmap_initialise(esl_link)
     
    -    logger.log(5, "Pretty Print Write Request Reply Packet:\n" + pretty_print_packet(rx_buffer))
    +    LOGGER.info("RMAP connection opened successfully.")
     
    -    return terminator, rx_buffer
    -
    -
    -
    -def update_transaction_identifier(rmap_link) ‑> int -
    -
    -

    Updates the transaction identifier and returns the new value.

    -

    Args

    -
    -
    rmap_link : ESL_RMAP
    -
    the RMAP link connection
    -
    -

    Returns

    -

    the updated transaction identifier (int).

    -
    - -Expand source code - -
    def update_transaction_identifier(rmap_link) -> int:
    -    """
    -    Updates the transaction identifier and returns the new value.
    -
    -    Args:
    -        rmap_link (ESL_RMAP): the RMAP link connection
    -
    -    Returns:
    -        the updated transaction identifier (int).
    -    """
    -    tid = rmap_link.contents.transaction_identifier
    -    tid = (tid + 1) & 0xFFFF
    -    rmap_link.contents.transaction_identifier = tid
    -    return tid
    + return rmap_link
    @@ -2643,140 +939,87 @@

    Returns

    Classes

    -
    -class CheckError -(message, status) -
    -
    -

    Raised when a check fails and you want to pass a status values along with the message.

    -
    - -Expand source code - -
    class CheckError(RMAPError):
    -    """
    -    Raised when a check fails and you want to pass a status values along with the message.
    -    """
    -
    -    def __init__(self, message, status):
    -        self.message = message
    -        self.status  = status
    -
    -

    Ancestors

    -
      -
    • RMAPError
    • -
    • builtins.Exception
    • -
    • builtins.BaseException
    • -
    -
    class ESL_RMAP -(*args, **kwargs)
    -

    Structure base class

    +

    ESL_RMAP()

    Expand source code -
    class ESL_RMAP(Structure):
    -    _fields_ = [
    -        ("spw_device",                esl_p),
    -        ("target_key",                c_ubyte),
    -        ("target_logical_address",    c_ubyte),
    -        ("target_spw_address",        c_ubyte * 12),
    -        ("target_spw_address_len",    c_int),
    -        ("reply_spw_address",         c_ubyte * 12),
    -        ("reply_spw_address_len",     c_int),
    -        ("initiator_logical_address", c_ubyte),
    -        ("transaction_identifier",    c_uint),
    -        ("ESL_RMAP_error",            c_int)
    -    ]
    +
    class ESL_RMAP:
    +    spw_device = None
    +    target_key = 0
    +    target_logical_address = 0xFE
    +    target_spw_address = 0x00
    +    target_spw_address_len = 0
    +    reply_spw_address = 0x00
    +    reply_spw_address_len = 0
    +    initiator_logical_address = 0xFE
    +    transaction_identifier = 99
    +    ESL_RMAP_error = 0
    +
    +    def __str__(self):
    +        return textwrap.dedent(
    +            f"""\
    +            spw_device={self.spw_device}
    +            target_key={self.target_key}
    +            target_logical_address={self.target_logical_address}
    +            target_spw_address={self.target_spw_address}
    +            target_spw_address_len={self.target_spw_address_len}
    +            reply_spw_address={self.reply_spw_address}
    +            reply_spw_address_len={self.reply_spw_address_len}
    +            initiator_logical_address={self.initiator_logical_address}
    +            transaction_identifier={self.transaction_identifier}
    +            ESL_RMAP_error={self.ESL_RMAP_error}
    +            """
    +        )
    -

    Ancestors

    -
      -
    • _ctypes.Structure
    • -
    • _ctypes._CData
    • -
    -

    Instance variables

    +

    Class variables

    var ESL_RMAP_error
    -

    Structure/Union member

    +
    var initiator_logical_address
    -

    Structure/Union member

    +
    var reply_spw_address
    -

    Structure/Union member

    +
    var reply_spw_address_len
    -

    Structure/Union member

    +
    var spw_device
    -

    Structure/Union member

    +
    var target_key
    -

    Structure/Union member

    +
    var target_logical_address
    -

    Structure/Union member

    +
    var target_spw_address
    -

    Structure/Union member

    +
    var target_spw_address_len
    -

    Structure/Union member

    +
    var transaction_identifier
    -

    Structure/Union member

    +
    -
    -class rmap_p -(*args, **kwargs) -
    -
    -

    XXX to be provided

    -

    Ancestors

    -
      -
    • _ctypes._Pointer
    • -
    • _ctypes._CData
    • -
    -
    -
    -class RMAPError -(*args, **kwargs) -
    -
    -

    Common base class for all non-exit exceptions.

    -
    - -Expand source code - -
    class RMAPError(Exception):
    -    pass
    -
    -

    Ancestors

    -
      -
    • builtins.Exception
    • -
    • builtins.BaseException
    • -
    -

    Subclasses

    - -
    @@ -2793,62 +1036,17 @@

    Index

  • Functions

  • Classes

  • diff --git a/docs/api/egse/dsi/rmapci.html b/docs/api/egse/dsi/rmapci.html index eb61688..80124f2 100644 --- a/docs/api/egse/dsi/rmapci.html +++ b/docs/api/egse/dsi/rmapci.html @@ -31,21 +31,23 @@

    Module egse.dsi.rmapci

    # This file implements functions to easily create the packets send over the RMAP protocol # for the DPU-FEE interface. # -# Please check the document PLATO-DLR-PL-IC-0002 v1.0 [25/09/2018] for reference. -# The RMAP Verified Write Request Packet STructure is defined on page 26 with req. FEE-DPU-IF-582. +# Please check the document PLATO-DLR-PL-IC-0002 v1.0 [25/09/2018] for reference. +# The RMAP Verified Write Request Packet Structure is defined on page 26 with req. FEE-DPU-IF-582. -import egse.dsi.rmap +from egse.dsi.rmap import ESL_RMAP +from egse.dsi.constants import RMAP_PROTOCOL_ID +from egse.rmap import rmap_crc_check # FIXME: We could just use the rmap_write(..) command for this, but that currently doesn't have verify before write etc. # It more looks like the RMAP interface to the device can also (better) be implemented in Python. -def write_critical_configuration_data(rmap_link, buf, tid, addr, data): - buf[0] = egse.dsi.rmap.rmap_get_target_logical_address(rmap_link) - buf[1] = egse.dsi.constants.RMAP_PROTOCOL_ID +def write_critical_configuration_data(rmap_link: ESL_RMAP, buf, tid, addr, data): + buf[0] = rmap_link.target_logical_address + buf[1] = RMAP_PROTOCOL_ID buf[2] = 0x7C # Instruction: RMAP Request, write, incrementing address, verify before write, and send reply, reply addr length=0 buf[3] = 0xD1 # Key: - buf[4] = egse.dsi.rmap.rmap_get_initiator_logical_address(rmap_link) + buf[4] = rmap_link.initiator_logical_address buf[5] = (tid >> 8) & 0xFF # MSB of the Transition ID buf[6] = (tid) & 0xFF # LSB of the Transition ID buf[7] = (addr >> 32) & 0xFF # Extended address @@ -56,12 +58,12 @@

    Module egse.dsi.rmapci

    buf[12] = 0x00 # data length (MSB) buf[13] = 0x00 # data length, fixed to 4 bytes for critical configuration parameters buf[14] = 0x04 # data length (LSB) - buf[15] = egse.dsi.rmap.rmap_crc_check(buf, 0, 15) + buf[15] = rmap_crc_check(buf, 0, 15) buf[16] = (data >> 24) & 0xFF # data (MSB) buf[17] = (data >> 16) & 0xFF # data buf[18] = (data >> 8) & 0xFF # data buf[19] = (data ) & 0xFF # data (LSB) - buf[20] = egse.dsi.rmap.rmap_crc_check(buf, 16, 4) + buf[20] = rmap_crc_check(buf, 16, 4) return buf
    @@ -73,7 +75,7 @@

    Module egse.dsi.rmapci

    Functions

    -def write_critical_configuration_data(rmap_link, buf, tid, addr, data) +def write_critical_configuration_data(rmap_link: ESL_RMAP, buf, tid, addr, data)
    @@ -81,12 +83,12 @@

    Functions

    Expand source code -
    def write_critical_configuration_data(rmap_link, buf, tid, addr, data):
    -    buf[0]  = egse.dsi.rmap.rmap_get_target_logical_address(rmap_link)
    -    buf[1]  = egse.dsi.constants.RMAP_PROTOCOL_ID
    +
    def write_critical_configuration_data(rmap_link: ESL_RMAP, buf, tid, addr, data):
    +    buf[0]  = rmap_link.target_logical_address
    +    buf[1]  = RMAP_PROTOCOL_ID
         buf[2]  = 0x7C                    # Instruction: RMAP Request, write, incrementing address, verify before write, and send reply, reply addr length=0
         buf[3]  = 0xD1                    # Key:
    -    buf[4]  = egse.dsi.rmap.rmap_get_initiator_logical_address(rmap_link)
    +    buf[4]  = rmap_link.initiator_logical_address
         buf[5]  = (tid >> 8) & 0xFF       # MSB of the Transition ID
         buf[6]  = (tid) & 0xFF            # LSB of the Transition ID
         buf[7]  = (addr >> 32) & 0xFF     # Extended address
    @@ -97,12 +99,12 @@ 

    Functions

    buf[12] = 0x00 # data length (MSB) buf[13] = 0x00 # data length, fixed to 4 bytes for critical configuration parameters buf[14] = 0x04 # data length (LSB) - buf[15] = egse.dsi.rmap.rmap_crc_check(buf, 0, 15) + buf[15] = rmap_crc_check(buf, 0, 15) buf[16] = (data >> 24) & 0xFF # data (MSB) buf[17] = (data >> 16) & 0xFF # data buf[18] = (data >> 8) & 0xFF # data buf[19] = (data ) & 0xFF # data (LSB) - buf[20] = egse.dsi.rmap.rmap_crc_check(buf, 16, 4) + buf[20] = rmap_crc_check(buf, 16, 4) return buf
    diff --git a/docs/api/egse/dsi/spw.html b/docs/api/egse/dsi/spw.html index a947ea8..1c70817 100644 --- a/docs/api/egse/dsi/spw.html +++ b/docs/api/egse/dsi/spw.html @@ -30,27 +30,41 @@

    Module egse.dsi.spw

    """
     This module contains functions to handle SpaceWire communication.
     """
    +from __future__ import annotations
     
     import logging
    +import socket
    +import textwrap
     from typing import Tuple
     
    +import egse.rmap
    +import egse.spw
     from egse.dsi import constants
    +from egse.dsi.constants import esl_rmap_error_codes
    +from egse.dsi.esl import ESLError
     from egse.dsi.esl import esl_close_connection
     from egse.dsi.esl import esl_configure
     from egse.dsi.esl import esl_flush
    +from egse.dsi.esl import esl_get_active_link
    +from egse.dsi.esl import esl_get_hwa
    +from egse.dsi.esl import esl_get_manufacturer_string
    +from egse.dsi.esl import esl_get_number_of_links
    +from egse.dsi.esl import esl_get_product_string
    +from egse.dsi.esl import esl_get_serial_number
     from egse.dsi.esl import esl_open_connection
     from egse.dsi.esl import esl_read_packet
     from egse.dsi.esl import esl_send_timecode
    +from egse.dsi.esl import esl_set_active_link
     from egse.dsi.esl import esl_write_packet
     from egse.dsi.rmap import rmap_configure
     from egse.dsi.rmap import rmap_open_connection
    -from egse.dsi.rmap import rmap_read_request
    -from egse.dsi.rmap import rmap_write_request
     from egse.settings import Settings
     from egse.spw import SpaceWireInterface
     from egse.spw import SpaceWirePacket
    +from egse.spw import WriteRequestReply
    +from egse.spw import ReadRequestReply
     
    -logger = logging.getLogger(__name__)
    +LOGGER = logging.getLogger(__name__)
     
     DSI_SETTINGS = Settings.load("DSI")
     
    @@ -78,16 +92,17 @@ 

    Module egse.dsi.spw

    Nothing: yet. """ - logger.debug("*" * 80) - logger.debug("Extension Packet returned by DSI") - logger.debug( + LOGGER.debug("*" * 80) + LOGGER.debug("Extension Packet returned by DSI") + LOGGER.debug( f"extension packet: {bin(int.from_bytes(rx_buffer, 'big'))} (length={bytes_received})" ) - logger.debug("*" * 80) + LOGGER.debug("*" * 80) def handle_special_packet(rx_buffer: bytes, bytes_received: int): """ + Decide how to handle a special packet that was received over the SpaceWire interface. Args: rx_buffer (bytes): the packet that was received as a bytes object @@ -97,8 +112,10 @@

    Module egse.dsi.spw

    Nothing: yet. """ - logger.debug("Special Packet returned by DSI") - logger.debug(f"extension packet: {rx_buffer}") + LOGGER.debug("*" * 80) + LOGGER.debug("Special Packet returned by DSI") + LOGGER.debug(f"Special packet: {rx_buffer} (length={bytes_received})") + LOGGER.debug("*" * 80) class SpaceWireOverDSI(SpaceWireInterface): @@ -107,15 +124,34 @@

    Module egse.dsi.spw

    SpaceWire Interface (DSI). """ - def __init__(self, dsi_address, dsi_port): + def __init__(self, dsi_address, dsi_port, esl_link=None, rmap_link=None): self.dsi_address = dsi_address self.dsi_port = dsi_port - self.esl_link = None - self.rmap_link = None + self.esl_link = esl_link + self.rmap_link = rmap_link + self.socket = None + + def __str__(self): + hwa = esl_get_hwa(self.esl_link) + serial_number = esl_get_serial_number(self.esl_link) + + msg = textwrap.dedent(f"""\ + Info on SpaceWire Interface: + Manufacturer {esl_get_manufacturer_string(self.esl_link)} + Product {esl_get_product_string(self.esl_link)} + Number of links {esl_get_number_of_links(self.esl_link)} [active port={esl_get_active_link(self.esl_link)}] + Serial number {serial_number} + Hardware Address 0x{hwa[0]:02X}-{hwa[1]:02X}-{hwa[2]:02X}-{hwa[3]:02X}-{hwa[4]:02X}-{hwa[5]:02X} + """ + ) + + return msg def connect(self): - self.esl_link = esl_open_connection(self.dsi_address) - self.rmap_link = rmap_open_connection(self.esl_link) + if self.esl_link is None: + self.esl_link = esl_open_connection(self.dsi_address) + if self.rmap_link is None: + self.rmap_link = rmap_open_connection(self.esl_link) # esl_print_info(self.esl_link) def disconnect(self): @@ -144,43 +180,188 @@

    Module egse.dsi.spw

    # esl_print_summary_of_structure(self.esl_link) + def get_socket(self): + """ + Returns the TCP socket that is associated with this transport. + + Returns: + A TCP socket or None when the transport has not been initialized or connected. + """ + # This 'trick' of getting the socket from the `fileno` works only once, so, if we have already + # requested the socket for this `fileno`, don't do it again. It will either result in a + # `OSError: [Errno 9] Bad file descriptor` or I've seen that a new socket created with the same + # family and type will reuse the same `fileno` down the line... + + if self.socket is None and self.esl_link is not None: + sock_fd = self.esl_link.contents.sock + self.socket = socket.socket(fileno=sock_fd) # This is how to get the socket from the esl_connection. + + return self.socket + + def set_active_link(self, port: int): + """ + Sets the active SpW port on the DSI. + + Raises: + An ESLError when an invalid link is provided. + + Returns: + The return code of the library call. + """ + rc = esl_set_active_link(self.esl_link, port) + if rc == constants.ESL_API_INVALID_LINK: # noqa + raise ESLError( + f"Could not set active link {port}, ESL error: Invalid Link selected [-45]." + ) + + return rc + + def get_active_link(self): + """Return the number of the currently active SpW port on the DSI.""" + return esl_get_active_link(self.esl_link) + def flush(self): esl_flush(self.esl_link) - def send_timecode(self, timecode: int): - esl_send_timecode(self.esl_link, timecode) + def send_timecode(self, timecode: int) -> int: + return esl_send_timecode(self.esl_link, timecode) def read_packet(self, timeout: int = None) -> Tuple[int, bytes]: return esl_read_packet(self.esl_link, timeout=timeout) - def write_packet(self, packet: bytes): - esl_write_packet(self.esl_link, packet, len(packet), constants.ESL_EOP) + def write_packet(self, packet: bytes) -> int: + return esl_write_packet(self.esl_link, packet, len(packet), constants.ESL_EOP) # noqa def read_register(self, address: int, length: int = 4, strict: bool = True) -> bytes: - terminator, rx_buffer = rmap_read_request(self.rmap_link, address, length) + reply_packet = self.rmap_read_request(address, length, strict=strict) + + if reply_packet is None: + LOGGER.critical(f"read_register() failed: An error occurred in the rmap_read_request() call.") + return bytes() + + return reply_packet.data + + def write_register(self, address: int, data: bytes) -> int: + reply_packet = self.rmap_write_request(address, data) + + if reply_packet is None: + LOGGER.critical(f"write_register() failed: An error occurred in the rmap_write_request() call.") + return -1 + + if reply_packet.status: + LOGGER.warning(f"write_register() failed: An error occurred in the rmap_write_request() call: " + f"{reply_packet.status = }") + + return reply_packet.status + + def read_memory_map(self, address: int, size: int) -> bytes: + reply_packet = self.rmap_read_request(address, size, strict=False) + + if reply_packet is None: + LOGGER.critical(f"read_memory_map(): An error occurred in the rmap_read_request() call.") + return bytes() + + return reply_packet.data + + def rmap_read_request( + self, address: int, length: int, timeout: int = 1000, strict: bool = True) -> ReadRequestReply | None: + """ + Read `length` bytes from the remote memory starting at `address`. - packet = SpaceWirePacket.create_packet(rx_buffer) - rx_data = packet.data + Args: + address: the start address (32-bit aligned) in the remote memory + length: the number of bytes to read from the remote memory + timeout: timeout in milliseconds + strict: perform strict checking of read areas - return rx_data + Returns: + A tuple containing the terminator value and the RMAP Reply packet with the data read from + the remote memory. - def write_register(self, address: int, data: bytes): - terminator, rx_buffer = rmap_write_request(self.rmap_link, address, data) + """ + # TODO: + # the timeout parameter is currently not implemented - # FIXME: - # So, what do we need to do with the terminator value and the rx_buffer? - # Is there some error checking needed here and what should than be the - # return value? + self.rmap_link.transaction_identifier = egse.spw.update_transaction_identifier(self.rmap_link.transaction_identifier) - return 0 + buffer = egse.rmap.create_rmap_read_request_packet( + address, length, self.rmap_link.transaction_identifier, strict=strict) - def read_memory_map(self, address: int, size: int): - terminator, rx_buffer = rmap_read_request(self.rmap_link, address, size) + result = esl_write_packet(self.rmap_link.spw_device, buffer, len(buffer), constants.ESL_EOP) # noqa + if result: + raise egse.rmap.RMAPError( + f"Couldn't send data within timeout of {timeout} ms, " + f"ESL RMAP Error Code = {esl_rmap_error_codes[self.rmap_link.ESL_RMAP_error]} [" + f"{self.rmap_link.ESL_RMAP_error}]") - packet = SpaceWirePacket.create_packet(rx_buffer) - rx_data = packet.data + result = esl_flush(self.rmap_link.spw_device) + if result: + raise egse.rmap.RMAPError( + f"Couldn't send data or clear buffers, " + f"ESL RMAP Error Code = {esl_rmap_error_codes[self.rmap_link.ESL_RMAP_error]} [" + f"{self.rmap_link.ESL_RMAP_error}]") - return rx_data
    + # Read the read request reply packet + + _, rx_buffer = esl_read_packet(self.rmap_link.spw_device, timeout=timeout) + + if rx_buffer: + reply_packet = SpaceWirePacket.create_packet(rx_buffer) + return reply_packet if isinstance(reply_packet, ReadRequestReply) else None + + return None + + def rmap_write_request( + self, address: int, data: bytes, length: int = 4, timeout: int = 1000) -> WriteRequestReply | None: + """ + Sends an RMAP write command over the SpaceWire link. + + Based on the address this function will decided to send a verified or unverified write request. + + TODO: the timeout parameter is currently not implemented. + + Args: + address: the starting memory address to which the data from buffer will be written + data: the data that will be written into the targets memory + length: the number of bytes to write (the buffer maybe longer) [default=4] + timeout: timeout in milliseconds [default=1000] + + Returns: + The Write Request Reply packet. + + Raises: + egse.rmap.RMAPError: when data can not be written on the target. + + """ + + self.rmap_link.transaction_identifier = egse.spw.update_transaction_identifier( + self.rmap_link.transaction_identifier + ) + + if egse.rmap.CRITICAL_AREA_START <= address <= egse.rmap.CRITICAL_AREA_END: + buffer = egse.rmap.create_rmap_verified_write_packet( + address, data, self.rmap_link.transaction_identifier) + else: + buffer = egse.rmap.create_rmap_unverified_write_packet( + address, data, length, self.rmap_link.transaction_identifier) + + result = esl_write_packet(self.esl_link, buffer, len(buffer), constants.ESL_EOP) # noqa + if result: + raise egse.rmap.RMAPError(f"Couldn't send data ({len(buffer)} bytes) within timeout of {timeout} ms.") + + result = esl_flush(self.esl_link) + if result: + raise egse.rmap.RMAPError(f"Couldn't send data or flush buffers.") + + # Read the write request reply message + + _, rx_buffer = esl_read_packet(self.esl_link) + + if rx_buffer: + reply_packet = SpaceWirePacket.create_packet(rx_buffer) + return reply_packet if isinstance(reply_packet, WriteRequestReply) else None + + return None
    @@ -231,19 +412,20 @@

    Returns

    Nothing: yet. """ - logger.debug("*" * 80) - logger.debug("Extension Packet returned by DSI") - logger.debug( + LOGGER.debug("*" * 80) + LOGGER.debug("Extension Packet returned by DSI") + LOGGER.debug( f"extension packet: {bin(int.from_bytes(rx_buffer, 'big'))} (length={bytes_received})" ) - logger.debug("*" * 80)
    + LOGGER.debug("*" * 80)
    def handle_special_packet(rx_buffer: bytes, bytes_received: int)
    -

    Args

    +

    Decide how to handle a special packet that was received over the SpaceWire interface.

    +

    Args

    rx_buffer : bytes
    the packet that was received as a bytes object
    @@ -261,6 +443,7 @@

    Returns

    def handle_special_packet(rx_buffer: bytes, bytes_received: int):
         """
    +    Decide how to handle a special packet that was received over the SpaceWire interface.
     
         Args:
             rx_buffer (bytes): the packet that was received as a bytes object
    @@ -270,8 +453,10 @@ 

    Returns

    Nothing: yet. """ - logger.debug("Special Packet returned by DSI") - logger.debug(f"extension packet: {rx_buffer}")
    + LOGGER.debug("*" * 80) + LOGGER.debug("Special Packet returned by DSI") + LOGGER.debug(f"Special packet: {rx_buffer} (length={bytes_received})") + LOGGER.debug("*" * 80)
    @@ -281,7 +466,7 @@

    Classes

    class SpaceWireOverDSI -(dsi_address, dsi_port) +(dsi_address, dsi_port, esl_link=None, rmap_link=None)

    The SpaceWireOverDSI implements the SpaceWire communication/transport over a Diagnostic @@ -296,15 +481,34 @@

    Classes

    SpaceWire Interface (DSI). """ - def __init__(self, dsi_address, dsi_port): + def __init__(self, dsi_address, dsi_port, esl_link=None, rmap_link=None): self.dsi_address = dsi_address self.dsi_port = dsi_port - self.esl_link = None - self.rmap_link = None + self.esl_link = esl_link + self.rmap_link = rmap_link + self.socket = None + + def __str__(self): + hwa = esl_get_hwa(self.esl_link) + serial_number = esl_get_serial_number(self.esl_link) + + msg = textwrap.dedent(f"""\ + Info on SpaceWire Interface: + Manufacturer {esl_get_manufacturer_string(self.esl_link)} + Product {esl_get_product_string(self.esl_link)} + Number of links {esl_get_number_of_links(self.esl_link)} [active port={esl_get_active_link(self.esl_link)}] + Serial number {serial_number} + Hardware Address 0x{hwa[0]:02X}-{hwa[1]:02X}-{hwa[2]:02X}-{hwa[3]:02X}-{hwa[4]:02X}-{hwa[5]:02X} + """ + ) + + return msg def connect(self): - self.esl_link = esl_open_connection(self.dsi_address) - self.rmap_link = rmap_open_connection(self.esl_link) + if self.esl_link is None: + self.esl_link = esl_open_connection(self.dsi_address) + if self.rmap_link is None: + self.rmap_link = rmap_open_connection(self.esl_link) # esl_print_info(self.esl_link) def disconnect(self): @@ -333,43 +537,188 @@

    Classes

    # esl_print_summary_of_structure(self.esl_link) + def get_socket(self): + """ + Returns the TCP socket that is associated with this transport. + + Returns: + A TCP socket or None when the transport has not been initialized or connected. + """ + # This 'trick' of getting the socket from the `fileno` works only once, so, if we have already + # requested the socket for this `fileno`, don't do it again. It will either result in a + # `OSError: [Errno 9] Bad file descriptor` or I've seen that a new socket created with the same + # family and type will reuse the same `fileno` down the line... + + if self.socket is None and self.esl_link is not None: + sock_fd = self.esl_link.contents.sock + self.socket = socket.socket(fileno=sock_fd) # This is how to get the socket from the esl_connection. + + return self.socket + + def set_active_link(self, port: int): + """ + Sets the active SpW port on the DSI. + + Raises: + An ESLError when an invalid link is provided. + + Returns: + The return code of the library call. + """ + rc = esl_set_active_link(self.esl_link, port) + if rc == constants.ESL_API_INVALID_LINK: # noqa + raise ESLError( + f"Could not set active link {port}, ESL error: Invalid Link selected [-45]." + ) + + return rc + + def get_active_link(self): + """Return the number of the currently active SpW port on the DSI.""" + return esl_get_active_link(self.esl_link) + def flush(self): esl_flush(self.esl_link) - def send_timecode(self, timecode: int): - esl_send_timecode(self.esl_link, timecode) + def send_timecode(self, timecode: int) -> int: + return esl_send_timecode(self.esl_link, timecode) def read_packet(self, timeout: int = None) -> Tuple[int, bytes]: return esl_read_packet(self.esl_link, timeout=timeout) - def write_packet(self, packet: bytes): - esl_write_packet(self.esl_link, packet, len(packet), constants.ESL_EOP) + def write_packet(self, packet: bytes) -> int: + return esl_write_packet(self.esl_link, packet, len(packet), constants.ESL_EOP) # noqa def read_register(self, address: int, length: int = 4, strict: bool = True) -> bytes: - terminator, rx_buffer = rmap_read_request(self.rmap_link, address, length) + reply_packet = self.rmap_read_request(address, length, strict=strict) + + if reply_packet is None: + LOGGER.critical(f"read_register() failed: An error occurred in the rmap_read_request() call.") + return bytes() + + return reply_packet.data + + def write_register(self, address: int, data: bytes) -> int: + reply_packet = self.rmap_write_request(address, data) + + if reply_packet is None: + LOGGER.critical(f"write_register() failed: An error occurred in the rmap_write_request() call.") + return -1 + + if reply_packet.status: + LOGGER.warning(f"write_register() failed: An error occurred in the rmap_write_request() call: " + f"{reply_packet.status = }") + + return reply_packet.status + + def read_memory_map(self, address: int, size: int) -> bytes: + reply_packet = self.rmap_read_request(address, size, strict=False) + + if reply_packet is None: + LOGGER.critical(f"read_memory_map(): An error occurred in the rmap_read_request() call.") + return bytes() + + return reply_packet.data + + def rmap_read_request( + self, address: int, length: int, timeout: int = 1000, strict: bool = True) -> ReadRequestReply | None: + """ + Read `length` bytes from the remote memory starting at `address`. + + Args: + address: the start address (32-bit aligned) in the remote memory + length: the number of bytes to read from the remote memory + timeout: timeout in milliseconds + strict: perform strict checking of read areas + + Returns: + A tuple containing the terminator value and the RMAP Reply packet with the data read from + the remote memory. - packet = SpaceWirePacket.create_packet(rx_buffer) - rx_data = packet.data + """ + # TODO: + # the timeout parameter is currently not implemented - return rx_data + self.rmap_link.transaction_identifier = egse.spw.update_transaction_identifier(self.rmap_link.transaction_identifier) - def write_register(self, address: int, data: bytes): - terminator, rx_buffer = rmap_write_request(self.rmap_link, address, data) + buffer = egse.rmap.create_rmap_read_request_packet( + address, length, self.rmap_link.transaction_identifier, strict=strict) - # FIXME: - # So, what do we need to do with the terminator value and the rx_buffer? - # Is there some error checking needed here and what should than be the - # return value? + result = esl_write_packet(self.rmap_link.spw_device, buffer, len(buffer), constants.ESL_EOP) # noqa + if result: + raise egse.rmap.RMAPError( + f"Couldn't send data within timeout of {timeout} ms, " + f"ESL RMAP Error Code = {esl_rmap_error_codes[self.rmap_link.ESL_RMAP_error]} [" + f"{self.rmap_link.ESL_RMAP_error}]") - return 0 + result = esl_flush(self.rmap_link.spw_device) + if result: + raise egse.rmap.RMAPError( + f"Couldn't send data or clear buffers, " + f"ESL RMAP Error Code = {esl_rmap_error_codes[self.rmap_link.ESL_RMAP_error]} [" + f"{self.rmap_link.ESL_RMAP_error}]") - def read_memory_map(self, address: int, size: int): - terminator, rx_buffer = rmap_read_request(self.rmap_link, address, size) + # Read the read request reply packet - packet = SpaceWirePacket.create_packet(rx_buffer) - rx_data = packet.data + _, rx_buffer = esl_read_packet(self.rmap_link.spw_device, timeout=timeout) - return rx_data
    + if rx_buffer: + reply_packet = SpaceWirePacket.create_packet(rx_buffer) + return reply_packet if isinstance(reply_packet, ReadRequestReply) else None + + return None + + def rmap_write_request( + self, address: int, data: bytes, length: int = 4, timeout: int = 1000) -> WriteRequestReply | None: + """ + Sends an RMAP write command over the SpaceWire link. + + Based on the address this function will decided to send a verified or unverified write request. + + TODO: the timeout parameter is currently not implemented. + + Args: + address: the starting memory address to which the data from buffer will be written + data: the data that will be written into the targets memory + length: the number of bytes to write (the buffer maybe longer) [default=4] + timeout: timeout in milliseconds [default=1000] + + Returns: + The Write Request Reply packet. + + Raises: + egse.rmap.RMAPError: when data can not be written on the target. + + """ + + self.rmap_link.transaction_identifier = egse.spw.update_transaction_identifier( + self.rmap_link.transaction_identifier + ) + + if egse.rmap.CRITICAL_AREA_START <= address <= egse.rmap.CRITICAL_AREA_END: + buffer = egse.rmap.create_rmap_verified_write_packet( + address, data, self.rmap_link.transaction_identifier) + else: + buffer = egse.rmap.create_rmap_unverified_write_packet( + address, data, length, self.rmap_link.transaction_identifier) + + result = esl_write_packet(self.esl_link, buffer, len(buffer), constants.ESL_EOP) # noqa + if result: + raise egse.rmap.RMAPError(f"Couldn't send data ({len(buffer)} bytes) within timeout of {timeout} ms.") + + result = esl_flush(self.esl_link) + if result: + raise egse.rmap.RMAPError(f"Couldn't send data or flush buffers.") + + # Read the write request reply message + + _, rx_buffer = esl_read_packet(self.esl_link) + + if rx_buffer: + reply_packet = SpaceWirePacket.create_packet(rx_buffer) + return reply_packet if isinstance(reply_packet, WriteRequestReply) else None + + return None

    Ancestors

      @@ -420,8 +769,10 @@

      Methods

      Expand source code
      def connect(self):
      -    self.esl_link = esl_open_connection(self.dsi_address)
      -    self.rmap_link = rmap_open_connection(self.esl_link)
      +    if self.esl_link is None:
      +        self.esl_link = esl_open_connection(self.dsi_address)
      +    if self.rmap_link is None:
      +        self.rmap_link = rmap_open_connection(self.esl_link)
           # esl_print_info(self.esl_link)
    @@ -451,17 +802,234 @@

    Methods

    esl_flush(self.esl_link) -
    -def send_timecode(self, timecode: int) +
    -
    +

    Return the number of the currently active SpW port on the DSI.

    +
    + +Expand source code + +
    def get_active_link(self):
    +    """Return the number of the currently active SpW port on the DSI."""
    +    return esl_get_active_link(self.esl_link)
    +
    +
    +
    +def get_socket(self) +
    +
    +

    Returns the TCP socket that is associated with this transport.

    +

    Returns

    +

    A TCP socket or None when the transport has not been initialized or connected.

    +
    + +Expand source code + +
    def get_socket(self):
    +    """
    +    Returns the TCP socket that is associated with this transport.
    +
    +    Returns:
    +        A TCP socket or None when the transport has not been initialized or connected.
    +    """
    +    # This 'trick' of getting the socket from the `fileno` works only once, so, if we have already
    +    # requested the socket for this `fileno`, don't do it again. It will either result in a
    +    # `OSError: [Errno 9] Bad file descriptor` or I've seen that a new socket created with the same
    +    # family and type will reuse the same `fileno` down the line...
    +
    +    if self.socket is None and self.esl_link is not None:
    +        sock_fd = self.esl_link.contents.sock
    +        self.socket = socket.socket(fileno=sock_fd)  # This is how to get the socket from the esl_connection.
    +
    +    return self.socket
    +
    +
    +
    +def rmap_read_request(self, address: int, length: int, timeout: int = 1000, strict: bool = True) ‑> ReadRequestReply | None +
    +
    +

    Read length bytes from the remote memory starting at address.

    +

    Args

    +
    +
    address
    +
    the start address (32-bit aligned) in the remote memory
    +
    length
    +
    the number of bytes to read from the remote memory
    +
    timeout
    +
    timeout in milliseconds
    +
    strict
    +
    perform strict checking of read areas
    +
    +

    Returns

    +

    A tuple containing the terminator value and the RMAP Reply packet with the data read from +the remote memory.

    +
    + +Expand source code + +
    def rmap_read_request(
    +        self, address: int, length: int, timeout: int = 1000, strict: bool = True) -> ReadRequestReply | None:
    +    """
    +    Read `length` bytes from the remote memory starting at `address`.
    +
    +    Args:
    +        address: the start address (32-bit aligned) in the remote memory
    +        length: the number of bytes to read from the remote memory
    +        timeout: timeout in milliseconds
    +        strict: perform strict checking of read areas
    +
    +    Returns:
    +        A tuple containing the terminator value and the RMAP Reply packet with the data read from
    +        the remote memory.
    +
    +    """
    +    # TODO:
    +    #   the timeout parameter is currently not implemented
    +
    +    self.rmap_link.transaction_identifier = egse.spw.update_transaction_identifier(self.rmap_link.transaction_identifier)
    +
    +    buffer = egse.rmap.create_rmap_read_request_packet(
    +        address, length, self.rmap_link.transaction_identifier, strict=strict)
    +
    +    result = esl_write_packet(self.rmap_link.spw_device, buffer, len(buffer), constants.ESL_EOP)  # noqa
    +    if result:
    +        raise egse.rmap.RMAPError(
    +            f"Couldn't send data within timeout of {timeout} ms, "
    +            f"ESL RMAP Error Code = {esl_rmap_error_codes[self.rmap_link.ESL_RMAP_error]} ["
    +            f"{self.rmap_link.ESL_RMAP_error}]")
    +
    +    result = esl_flush(self.rmap_link.spw_device)
    +    if result:
    +        raise egse.rmap.RMAPError(
    +            f"Couldn't send data or clear buffers, "
    +            f"ESL RMAP Error Code = {esl_rmap_error_codes[self.rmap_link.ESL_RMAP_error]} ["
    +            f"{self.rmap_link.ESL_RMAP_error}]")
    +
    +    # Read the read request reply packet
    +
    +    _, rx_buffer = esl_read_packet(self.rmap_link.spw_device, timeout=timeout)
    +
    +    if rx_buffer:
    +        reply_packet = SpaceWirePacket.create_packet(rx_buffer)
    +        return reply_packet if isinstance(reply_packet, ReadRequestReply) else None
    +
    +    return None
    +
    +
    +
    +def rmap_write_request(self, address: int, data: bytes, length: int = 4, timeout: int = 1000) ‑> WriteRequestReply | None +
    +
    +

    Sends an RMAP write command over the SpaceWire link.

    +

    Based on the address this function will decided to send a verified or unverified write request.

    +

    TODO: the timeout parameter is currently not implemented.

    +

    Args

    +
    +
    address
    +
    the starting memory address to which the data from buffer will be written
    +
    data
    +
    the data that will be written into the targets memory
    +
    length
    +
    the number of bytes to write (the buffer maybe longer) [default=4]
    +
    timeout
    +
    timeout in milliseconds [default=1000]
    +
    +

    Returns

    +

    The Write Request Reply packet.

    +

    Raises

    +
    +
    RMAPError
    +
    when data can not be written on the target.
    +
    Expand source code -
    def send_timecode(self, timecode: int):
    -    esl_send_timecode(self.esl_link, timecode)
    +
    def rmap_write_request(
    +        self, address: int, data: bytes, length: int = 4, timeout: int = 1000) -> WriteRequestReply | None:
    +    """
    +    Sends an RMAP write command over the SpaceWire link.
    +
    +    Based on the address this function will decided to send a verified or unverified write request.
    +
    +    TODO: the timeout parameter is currently not implemented.
    +
    +    Args:
    +        address: the starting memory address to which the data from buffer will be written
    +        data: the data that will be written into the targets memory
    +        length: the number of bytes to write (the buffer maybe longer) [default=4]
    +        timeout: timeout in milliseconds [default=1000]
    +
    +    Returns:
    +        The Write Request Reply packet.
    +
    +    Raises:
    +        egse.rmap.RMAPError: when data can not be written on the target.
    +
    +    """
    +
    +    self.rmap_link.transaction_identifier = egse.spw.update_transaction_identifier(
    +        self.rmap_link.transaction_identifier
    +    )
    +
    +    if egse.rmap.CRITICAL_AREA_START <= address <= egse.rmap.CRITICAL_AREA_END:
    +        buffer = egse.rmap.create_rmap_verified_write_packet(
    +            address, data, self.rmap_link.transaction_identifier)
    +    else:
    +        buffer = egse.rmap.create_rmap_unverified_write_packet(
    +            address, data, length, self.rmap_link.transaction_identifier)
    +
    +    result = esl_write_packet(self.esl_link, buffer, len(buffer), constants.ESL_EOP)  # noqa
    +    if result:
    +        raise egse.rmap.RMAPError(f"Couldn't send data ({len(buffer)} bytes) within timeout of {timeout} ms.")
    +
    +    result = esl_flush(self.esl_link)
    +    if result:
    +        raise egse.rmap.RMAPError(f"Couldn't send data or flush buffers.")
    +
    +    # Read the write request reply message
    +
    +    _, rx_buffer = esl_read_packet(self.esl_link)
    +
    +    if rx_buffer:
    +        reply_packet = SpaceWirePacket.create_packet(rx_buffer)
    +        return reply_packet if isinstance(reply_packet, WriteRequestReply) else None
    +
    +    return None
    +
    +
    + +
    +

    Sets the active SpW port on the DSI.

    +

    Raises

    +

    An ESLError when an invalid link is provided.

    +

    Returns

    +

    The return code of the library call.

    +
    + +Expand source code + +
    def set_active_link(self, port: int):
    +    """
    +    Sets the active SpW port on the DSI.
    +
    +    Raises:
    +        An ESLError when an invalid link is provided.
    +
    +    Returns:
    +        The return code of the library call.
    +    """
    +    rc = esl_set_active_link(self.esl_link, port)
    +    if rc == constants.ESL_API_INVALID_LINK:  # noqa
    +        raise ESLError(
    +            f"Could not set active link {port}, ESL error: Invalid Link selected [-45]."
    +        )
    +
    +    return rc
    @@ -472,6 +1040,7 @@

    Inherited members

  • read_memory_map
  • read_packet
  • read_register
  • +
  • send_timecode
  • write_packet
  • write_register
  • @@ -502,12 +1071,16 @@

    Index

    +
  • Functions

    + +
  • Classes

    diff --git a/docs/api/egse/fdir/fdir_remote_popup.html b/docs/api/egse/fdir/fdir_remote_popup.html index 5d32b08..9979eee 100644 --- a/docs/api/egse/fdir/fdir_remote_popup.html +++ b/docs/api/egse/fdir/fdir_remote_popup.html @@ -30,11 +30,6 @@

    Module egse.fdir.fdir_remote_popup

    import sys from argparse import ArgumentParser -from egse.control import Response -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMessageBox, QApplication, QDialog, QLabel, QVBoxLayout -from PyQt5.QtGui import QFont - from egse.fdir import generate_popup # This function can be used generates a pop-up window when an FDIR situation was triggered. diff --git a/docs/api/egse/fdir/index.html b/docs/api/egse/fdir/index.html index 9aa9559..a125a05 100644 --- a/docs/api/egse/fdir/index.html +++ b/docs/api/egse/fdir/index.html @@ -27,9 +27,7 @@

    Module egse.fdir

    Expand source code
    from egse.control import Response
    -from PyQt5.QtCore import Qt
    -from PyQt5.QtWidgets import QMessageBox, QApplication, QDialog, QLabel, QVBoxLayout
    -from PyQt5.QtGui import QFont
    +from PyQt5.QtWidgets import QMessageBox, QApplication
     
     def generate_popup(code: str, actions: str, success: bool) -> Response:
         app = QApplication([])
    diff --git a/docs/api/egse/fee/feesim.html b/docs/api/egse/fee/feesim.html
    index 8b6e5ad..90e5a68 100644
    --- a/docs/api/egse/fee/feesim.html
    +++ b/docs/api/egse/fee/feesim.html
    @@ -65,30 +65,30 @@ 

    Module egse.fee.feesim

    import getpass import logging import multiprocessing - -import click -import numpy as np - -from egse.hk import TmDictionaryColumns -from egse.setup import SetupError -from egse.state import GlobalState - -multiprocessing.current_process().name = "feesim" - +import random import sys import time -from typing import NamedTuple, List +from typing import List +from typing import NamedTuple +import click +import numpy as np import sshtunnel from egse.dsi.spw import SpaceWireOverDSI -from egse.fee import n_fee_mode from egse.fee import convert_ccd_order_value -from egse.fee.fee import create_pattern_data -from egse.fee.fee import generate_data_packets +from egse.fee import n_fee_mode +from egse.fee.n_fee_hk import ORIGIN from egse.fee.nfee import HousekeepingData +from egse.hk import TmDictionaryColumns +from egse.randomwalk import RandomWalk from egse.reg import RegisterMap +from egse.rmap import create_rmap_read_request_reply_packet +from egse.rmap import create_rmap_write_request_reply_packet from egse.settings import Settings +from egse.setup import Setup +from egse.setup import SetupError +from egse.setup import load_setup from egse.spw import DataPacketHeader from egse.spw import DataPacketType from egse.spw import PacketType @@ -96,10 +96,8 @@

    Module egse.fee.feesim

    from egse.spw import SpaceWireInterface from egse.spw import SpaceWirePacket from egse.spw import WriteRequest -from egse.spw import create_rmap_read_request_reply_packet -from egse.spw import create_rmap_write_request_reply_packet +from egse.system import SignalCatcher from egse.zmq.spw import SpaceWireOverZeroMQ -from egse.fee.n_fee_hk import ORIGIN logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL) @@ -256,9 +254,9 @@

    Module egse.fee.feesim

    Returns: The sync period (time between two sync pulses) in seconds. """ - sync_sel = self.reg_map.get_value("reg_5_config", "sync_sel") + sync_sel = self.sync_sel if sync_sel: - sync_period = self.reg_map.get_value("reg_4_config", "int_sync_period") + sync_period = self.int_sync_period + 400 # add the duration of the long pulse else: sync_period = 6250 return sync_period / 1000 @@ -268,6 +266,213 @@

    Module egse.fee.feesim

    return convert_ccd_order_value(self.ccd_readout_order) +temperature_walks = { + "TOU_SENSE_1": RandomWalk(boundary=(0, 0x7FFF)), + "TOU_SENSE_2": RandomWalk(boundary=(0, 0x7FFF)), + "TOU_SENSE_3": RandomWalk(boundary=(0, 0x7FFF)), + "TOU_SENSE_4": RandomWalk(boundary=(0, 0x7FFF)), + "TOU_SENSE_5": RandomWalk(boundary=(0, 0x7FFF)), + "TOU_SENSE_6": RandomWalk(boundary=(0, 0x7FFF)), +} + + +def create_pattern_data(timecode: int, ccd_id: int, ccd_side: int) -> np.ndarray: + """ + Create pattern data as a two-dimensional ND array. The pattern data is generated as described + in PLATO-LESIA-PL-TN-023 - SimuCam pattern requirement. + + The pattern is build up like this: each pixel is a 16-bit number with the following structure: + + * Bits [15:13] = time-code % 8 + * Bits [12:11] = CCD number [1, 2, 3, 4] should be [0-3]! + * Bit [10] = CCD side: 0 = left side, 1 = right side + * Bits [9:5] = Y-coordinate % 32 + * Bits [4:0] = X-coordinate % 32 + + The total image data size of a full frame CCD is: + + x = 4590 = 2 x (25 [serial prescan] + 2255 + 15 [serial overscan]) + y = 4540 = 4510 + 30 [parallel overscan] + + This function creates each side of the CCD separately, so each half can be treated individually + as is done in the N-FEE. The two sides can easily be concatenated to form the full image: + + data = np.concatenate((data_left, data_right), axis=1) + + Args: + timecode (int): the timecode for this readout + ccd_id (int): the CCD number [0-3] + ccd_side (int): the CCD side, 0 = left = E-side, 1 = right = F-side + Returns: + A two-dimensional ND array representing half of a CCD. + """ + + ccd_number = ccd_id # save for later use + + timecode = (timecode % 8) << 13 # timecode is 3 bits at [15:13] + ccd_id = (ccd_id & 0b0011) << 11 # ccd_id is 2 bits at [12:11] + ccd_side = (ccd_side & 0b0001) << 10 # ccd_side is 1 bit at [10] + + x_size = 25 + 2255 + 15 + y_size = 4510 + 30 + + rows, cols = np.indices((y_size, x_size), dtype=np.uint16) + cols %= 32 + rows %= 32 + + data = rows * 16 + cols + timecode + ccd_side + ccd_id + + # We leave set the msb because of the bit flipt for N-FEE EM + + data[50:300, 100:105] = ccd_number | 0b10000000_00000000 + data[100:105, 50:500] = ccd_number | 0b10000000_00000000 + data[300:305, 50:150] = ccd_number | 0b10000000_00000000 + data[50:150, 500:505] = ccd_number | 0b10000000_00000000 + + # We unset the msb because of the bit flip for N-FEE EM + + data[110, 110] = 0x7FFF + + return data + + +def initialise_hk_data(hk_data: HousekeepingData) -> HousekeepingData: + """Initialises the housekeeping data to fake or simulated values.""" + + try: + setup = load_setup() + hk_info_table = setup.telemetry.dictionary + except AttributeError as exc: + raise SetupError("Version of the telemetry dictionary not specified in the current setup.") from exc + + storage_mnemonic_col = hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values + original_name_col = hk_info_table[TmDictionaryColumns.ORIGINAL_EGSE_HK_NAMES].values + selection = np.where(storage_mnemonic_col == ORIGIN) + original_names = original_name_col[selection] + + for name in temperature_walks: + hk_data[name] = int(next(temperature_walks[name])) + + hk_data["CCD1_TS"] = 0x7FFF + hk_data["CCD2_TS"] = 0x7FFF + hk_data["CCD3_TS"] = 0x7FFF + hk_data["CCD4_TS"] = 0x7FFF + + hk_data["PRT1"] = 0x7FFF + hk_data["PRT2"] = 0x7FFF + hk_data["PRT3"] = 0x7FFF + hk_data["PRT4"] = 0x7FFF + hk_data["PRT5"] = 0x7FFF + + hk_data["ZERO_DIFF_AMP"] = 0x8015 + + if "CCD1_VOD_MON" in original_names: + hk_data["CCD1_VOD_MON"] = 0x8055 + else: + if "CCD1_VOD_MON_E" in original_names: + hk_data["CCD1_VOD_MON_E"] = 0x8055 + if "CCD1_VOD_MON_F" in original_names: + hk_data["CCD1_VOD_MON_F"] = 0x8055 + hk_data["CCD1_VOG_MON"] = 0x8056 + hk_data["CCD1_VRD_MON_E"] = 0x8056 + + if "CCD2_VOD_MON" in original_names: + hk_data["CCD2_VOD_MON"] = 0x8057 + else: + if "CCD2_VOD_MON_E" in original_names: + hk_data["CCD2_VOD_MON_E"] = 0x8057 + if "CCD2_VOD_MON_F" in original_names: + hk_data["CCD2_VOD_MON_F"] = 0x8057 + hk_data["CCD2_VOG_MON"] = 0x8058 + hk_data["CCD2_VRD_MON_E"] = 0x8057 + + if "CCD3_VOD_MON" in original_names: + hk_data["CCD3_VOD_MON"] = 0x8058 + else: + if "CCD3_VOD_MON_E" in original_names: + hk_data["CCD3_VOD_MON_E"] = 0x8058 + if "CCD3_VOD_MON_F" in original_names: + hk_data["CCD3_VOD_MON_F"] = 0x8058 + hk_data["CCD3_VOG_MON"] = 0x8058 + hk_data["CCD3_VRD_MON_E"] = 0x8058 + + if "CCD4_VOD_MON" in original_names: + hk_data["CCD4_VOD_MON"] = 0x8057 + else: + if "CCD4_VOD_MON_E" in original_names: + hk_data["CCD4_VOD_MON_E"] = 0x8057 + if "CCD4_VOD_MON_F" in original_names: + hk_data["CCD4_VOD_MON_F"] = 0x8057 + hk_data["CCD4_VOG_MON"] = 0x8058 + hk_data["CCD4_VRD_MON_E"] = 0x8058 + + hk_data["VCCD"] = 0x39BF + hk_data["VRCLK_MON"] = 0xFC8A + hk_data["VICLK"] = 0xFAE9 + if "VRCLK_LOW" in original_names: + hk_data["VRCLK_LOW"] = 0x821A + + if "5VB_POS_MON" in original_names: + hk_data["5VB_POS_MON"] = 0x1E6A + hk_data["5VB_NEG_MON"] = 0x1A9F + hk_data["3V3B_MON"] = 0xE75D + hk_data["2V5A_MON"] = 0x1979 + hk_data["3V3D_MON"] = 0xE76E + hk_data["2V5D_MON"] = 0x1A8C + hk_data["1V5D_MON"] = 0xDF35 + hk_data["5VREF_MON"] = 0x1A80 + + hk_data["VCCD_POS_RAW"] = 0x53BF + hk_data["VCLK_POS_RAW"] = 0x40BA + hk_data["VAN1_POS_RAW"] = 0x0744 + hk_data["VAN3_NEG_MON"] = 0xFB7C + hk_data["VAN2_POS_RAW"] = 0x3AEC + hk_data["VDIG_RAW"] = 0x0AB5 + if "5VB_POS_MON" in original_names: + hk_data["VDIG_RAW_2"] = 0x0A32 + if "VICLK_LOW" in original_names: + hk_data["VICLK_LOW"] = 0x8277 + + hk_data["CCD1_VRD_MON_F"] = 0x8059 + hk_data["CCD1_VDD_MON"] = 0x94CA + hk_data["CCD1_VGD_MON"] = 0x8056 + hk_data["CCD2_VRD_MON_F"] = 0x8058 + hk_data["CCD2_VDD_MON"] = 0x94C1 + hk_data["CCD2_VGD_MON"] = 0x8055 + hk_data["CCD3_VRD_MON_F"] = 0x8059 + hk_data["CCD3_VDD_MON"] = 0x94C1 + hk_data["CCD3_VGD_MON"] = 0x8058 + hk_data["CCD4_VRD_MON_F"] = 0x8058 + hk_data["CCD4_VDD_MON"] = 0x94BA + hk_data["CCD4_VGD_MON"] = 0x8056 + + hk_data["IG_HI_MON"] = 0x8057 + if "IG_LO_MON" in original_names: + hk_data["IG_LO_MON"] = 0x8059 + hk_data["TSENSE_A"] = 0x8059 + hk_data["TSENSE_B"] = 0x805A + + hk_data["spw_timecode"] = 0x0000 + hk_data["rmap_target_status"] = 0x0000 + hk_data["rmap_target_indicate"] = 0x0000 + hk_data["spw_link_escape_error"] = 0x0000 + hk_data["spw_credit_error"] = 0x0000 + hk_data["spw_parity_error"] = 0x0000 + hk_data["spw_link_disconnect"] = 0x0000 + hk_data["spw_link_running"] = 0x0001 + + hk_data["frame_counter"] = 0x0000 + hk_data["op_mode"] = 0x0000 + hk_data["frame_number"] = 0x0000 + + hk_data["error_flags"] = 0x0000 + + hk_data["FPGA minor version"] = 0x0018 + hk_data["FPGA major version"] = 0x0000 + hk_data["Board ID"] = 0x0000 + + return hk_data + class FEESimulator: """ A software simulator for the front-end electronics of the PLATO cameras. @@ -283,148 +488,15 @@

    Module egse.fee.feesim

    self._hk_data = HousekeepingData() self._hk_header = DataPacketHeader() + self.setup = load_setup() + self._IN = NFEEInternals(self.register_map) - self.initialise_hk_data() + initialise_hk_data(self._hk_data) self.initialise_hk_header() - def initialise_hk_data(self): - """Initialises the housekeeping data to fake or simulated values.""" - - try: - hk_info_table = GlobalState.setup.telemetry.dictionary - except AttributeError: - raise SetupError("Version of the telemetry dictionary not specified in the current setup") - - storage_mnemonic_col = hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values - original_name_col = hk_info_table[TmDictionaryColumns.ORIGINAL_EGSE_HK_NAMES].values - selection = np.where(storage_mnemonic_col == ORIGIN) - original_names = original_name_col[selection] - - self._hk_data["TOU_SENSE_1"] = 0x8000 - self._hk_data["TOU_SENSE_2"] = 0x8000 - self._hk_data["TOU_SENSE_3"] = 0x8000 - self._hk_data["TOU_SENSE_4"] = 0x8000 - self._hk_data["TOU_SENSE_5"] = 0x8000 - self._hk_data["TOU_SENSE_6"] = 0x8000 - - self._hk_data["CCD1_TS"] = 0x7FFF - self._hk_data["CCD2_TS"] = 0x7FFF - self._hk_data["CCD3_TS"] = 0x7FFF - self._hk_data["CCD4_TS"] = 0x7FFF - - self._hk_data["PRT1"] = 0x7FFF - self._hk_data["PRT2"] = 0x7FFF - self._hk_data["PRT3"] = 0x7FFF - self._hk_data["PRT4"] = 0x7FFF - self._hk_data["PRT5"] = 0x7FFF - - self._hk_data["ZERO_DIFF_AMP"] = 0x8015 - - if "CCD1_VOD_MON" in original_names: - self._hk_data["CCD1_VOD_MON"] = 0x8055 - else: - if "CCD1_VOD_MON_E" in original_names: - self._hk_data["CCD1_VOD_MON_E"] = 0x8055 - if "CCD1_VOD_MON_F" in original_names: - self._hk_data["CCD1_VOD_MON_F"] = 0x8055 - self._hk_data["CCD1_VOG_MON"] = 0x8056 - self._hk_data["CCD1_VRD_MON_E"] = 0x8056 - - if "CCD2_VOD_MON" in original_names: - self._hk_data["CCD2_VOD_MON"] = 0x8057 - else: - if "CCD2_VOD_MON_E" in original_names: - self._hk_data["CCD2_VOD_MON_E"] = 0x8057 - if "CCD2_VOD_MON_E" in original_names: - self._hk_data["CCD2_VOD_MON_E"] = 0x8057 - self._hk_data["CCD2_VOG_MON"] = 0x8058 - self._hk_data["CCD2_VRD_MON_E"] = 0x8057 - - if "CCD3_VOD_MON" in original_names: - self._hk_data["CCD3_VOD_MON"] = 0x8058 - else: - if "CCD3_VOD_MON_E" in original_names: - self._hk_data["CCD3_VOD_MON_E"] = 0x8058 - if "CCD3_VOD_MON_F" in original_names: - self._hk_data["CCD3_VOD_MON_F"] = 0x8058 - self._hk_data["CCD3_VOG_MON"] = 0x8058 - self._hk_data["CCD3_VRD_MON_E"] = 0x8058 - - if "CCD4_VOD_MON" in original_names: - self._hk_data["CCD4_VOD_MON"] = 0x8057 - else: - if "CCD4_VOD_MON_E" in original_names: - self._hk_data["CCD4_VOD_MON_E"] = 0x8057 - if "CCD4_VOD_MON_F" in original_names: - self._hk_data["CCD4_VOD_MON_F"] = 0x8057 - self._hk_data["CCD4_VOG_MON"] = 0x8058 - self._hk_data["CCD4_VRD_MON_E"] = 0x8058 - - self._hk_data["VCCD"] = 0x39BF - self._hk_data["VRCLK_MON"] = 0xFC8A - self._hk_data["VICLK"] = 0xFAE9 - if "VRCLK_LOW" in original_names: - self._hk_data["VRCLK_LOW"] = 0x821A - - if "5VB_POS_MON" in original_names: - self._hk_data["5VB_POS_MON"] = 0x1E6A - self._hk_data["5VB_NEG_MON"] = 0x1A9F - self._hk_data["3V3B_MON"] = 0xE75D - self._hk_data["2V5A_MON"] = 0x1979 - self._hk_data["3V3D_MON"] = 0xE76E - self._hk_data["2V5D_MON"] = 0x1A8C - self._hk_data["1V5D_MON"] = 0xDF35 - self._hk_data["5VREF_MON"] = 0x1A80 - - self._hk_data["VCCD_POS_RAW"] = 0x53BF - self._hk_data["VCLK_POS_RAW"] = 0x40BA - self._hk_data["VAN1_POS_RAW"] = 0x0744 - self._hk_data["VAN3_NEG_MON"] = 0xFB7C - self._hk_data["VAN2_POS_RAW"] = 0x3AEC - self._hk_data["VDIG_RAW"] = 0x0AB5 - if "5VB_POS_MON" in original_names: - self._hk_data["VDIG_RAW_2"] = 0x0A32 - if "VICLK_LOW" in original_names: - self._hk_data["VICLK_LOW"] = 0x8277 - - self._hk_data["CCD1_VRD_MON_F"] = 0x8059 - self._hk_data["CCD1_VDD_MON"] = 0x94CA - self._hk_data["CCD1_VGD_MON"] = 0x8056 - self._hk_data["CCD2_VRD_MON_F"] = 0x8058 - self._hk_data["CCD2_VDD_MON"] = 0x94C1 - self._hk_data["CCD2_VGD_MON"] = 0x8055 - self._hk_data["CCD3_VRD_MON_F"] = 0x8059 - self._hk_data["CCD3_VDD_MON"] = 0x94C1 - self._hk_data["CCD3_VGD_MON"] = 0x8058 - self._hk_data["CCD4_VRD_MON_F"] = 0x8058 - self._hk_data["CCD4_VDD_MON"] = 0x94BA - self._hk_data["CCD4_VGD_MON"] = 0x8056 - - self._hk_data["IG_HI_MON"] = 0x8057 - if "IG_LO_MON" in original_names: - self._hk_data["IG_LO_MON"] = 0x8059 - self._hk_data["TSENSE_A"] = 0x8059 - self._hk_data["TSENSE_B"] = 0x805A - - self._hk_data["spw_timecode"] = 0x0000 - self._hk_data["rmap_target_status"] = 0x0000 - self._hk_data["rmap_target_indicate"] = 0x0000 - self._hk_data["spw_link_escape_error"] = 0x0000 - self._hk_data["spw_credit_error"] = 0x0000 - self._hk_data["spw_parity_error"] = 0x0000 - self._hk_data["spw_link_disconnect"] = 0x0000 - self._hk_data["spw_link_running"] = 0x0001 - - self._hk_data["frame_counter"] = 0x0000 - self._hk_data["op_mode"] = 0x0000 - self._hk_data["frame_number"] = 0x0000 - - self._hk_data["error_flags"] = 0x0000 - - self._hk_data["FPGA minor version"] = 0x0018 - self._hk_data["FPGA major version"] = 0x0000 - self._hk_data["Board ID"] = 0x0000 + self._killer = SignalCatcher() + def initialise_hk_header(self): """ @@ -489,8 +561,8 @@

    Module egse.fee.feesim

    def run(self): LOGGER.info(f"FEE Simulator Current State: {self.decode_current_state()}") - n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum - sensor_sel_enum = GlobalState.setup.camera.fee.sensor_sel.enum + n_fee_side = self.setup.camera.fee.ccd_sides.enum + sensor_sel_enum = self.setup.camera.fee.sensor_sel.enum with self._transport: @@ -508,15 +580,21 @@

    Module egse.fee.feesim

    sync_period = self.get_sync_period() + if self._killer.term_signal_received: + LOGGER.warning(f"Terminating N-FEE Simulator after receiving {self._killer.signal_name}") + break + if time.perf_counter() <= start_time + sync_period: continue + # LOGGER.debug(f"{'-'*80} {sync_period = }") + start_time = time.perf_counter() # Send a timecode packet ----------------------------------------------------------- timecode = self._hk_data.timecode - LOGGER.info(f"Sending timecode=0x{timecode:02X}") + LOGGER.info(f"Sending timecode: 0x{timecode:02X} ({timecode})") self._transport.send_timecode(timecode) # Send a housekeeping packet ------------------------------------------------------- @@ -524,7 +602,7 @@

    Module egse.fee.feesim

    LOGGER.info(f"Sending HK packet: frame_counter={self._hk_data.frame_counter}, " f"sequence_counter={self._hk_header.sequence_counter}, " f"type={self._hk_header.type_as_object}") - LOGGER.info(f"error_flags={self._hk_data['error_flags']}") + LOGGER.debug(f"HK Packet: error_flags=0b{self._hk_data['error_flags']:0b}") data = self._hk_header.data_as_bytes() + self._hk_data.data_as_bytes() packet = SpaceWirePacket.create_packet(data) @@ -532,6 +610,13 @@

    Module egse.fee.feesim

    self._transport.flush() + # Check if error flags must be cleared --------------------------------------------- + + if self.register_map['clear_error_flag']: + LOGGER.info("Clearing error flags") + self.register_map[('reg_21_config', 'clear_error_flag')] = 0 + self._hk_data['error_flags'] = 0 + # Send the Data packets ------------------------------------------------------------ LOGGER.info(f"mode={self.decode_current_state()}, " @@ -547,25 +632,39 @@

    Module egse.fee.feesim

    n_fee_mode.FULL_IMAGE_MODE,)): try: - ccd_id_to_bin = GlobalState.setup.camera.fee.ccd_numbering.CCD_ID_TO_BIN + ccd_id_to_bin = self.setup.camera.fee.ccd_numbering.CCD_ID_TO_BIN except AttributeError: raise SetupError("No entry in the setup for camera.fee.ccd_numbering.CCD_ID_TO_BIN") + LOGGER.info("Sending Data packets...") + ccd_number = ccd_id_to_bin[self.get_current_ccd_number()] # N-FEE CCD number [0-3] if self.sensor_sel & sensor_sel_enum.E_SIDE: - self.send_data_packets(timecode, ccd_id=ccd_number, ccd_side=n_fee_side.E_SIDE) + send_data_packets( + self._transport, timecode, + ccd_id=ccd_number, ccd_side=n_fee_side.E_SIDE, frame_number=self._hk_data.frame_number, + ccd_mode=self._IN.ccd_mode_config, v_start=self.v_start, v_end=self.v_end, setup=self.setup + ) if self.sensor_sel & sensor_sel_enum.F_SIDE: - self.send_data_packets(timecode, ccd_id=ccd_number, ccd_side=n_fee_side.F_SIDE) + send_data_packets( + self._transport, timecode, + ccd_id=ccd_number, ccd_side=n_fee_side.F_SIDE, frame_number=self._hk_data.frame_number, + ccd_mode=self._IN.ccd_mode_config, v_start=self.v_start, v_end=self.v_end, setup=self.setup + ) + + if self._killer.user_signal_received and self._killer.signal_name == "SIGUSR1": + # On USR1 signal, set the error flags to a random number. There are currently 12 error flags. + self.set_error_flags(random.randint(0x0, 0b1111_1111_1111)) + self._killer.clear() # Process any RMAP commands that were sent ----------------------------------------- - LOGGER.info( - f"Calling process_rmap_commands() method for " - f"{self._transport.__class__.__name__}." - ) + LOGGER.info("Processing RMAP commands...") while time.perf_counter() < start_time + sync_period - 0.2: - process_rmap_command(self._transport, self.register_map) + process_rmap_command(self._transport, self.register_map, self._hk_data) + + LOGGER.info("Updating internals...") self._hk_data.increment_timecode() @@ -580,34 +679,124 @@

    Module egse.fee.feesim

    self._IN.update(200) self.update_hk_header(200) - self.update_error_flags() - def update_error_flags(self): - """Set error_flags to 0b0000_1000_1000 (136) whenever the frame counter is a multiple of 5.""" - self._hk_data["error_flags"] = 0b0000_0000_0000 if self._hk_data.frame_counter % 5 else 0b0000_1000_1000 + def set_error_flags(self, flags: int): + """Set error_flags to the given number.""" + LOGGER.warning(f"Setting error flags to 0b{flags:012b}") + self._hk_data["error_flags"] = flags - def send_data_packets(self, timecode: int, ccd_id: int, ccd_side: int): - data = create_pattern_data(timecode, ccd_id, ccd_side) +def generate_data_packets(data: np.ndarray, header: DataPacketHeader, v_start: int, v_end: int, setup: Setup): + """ + This generator function creates and returns the SpaceWire packets to send to the DPU Processor. - header = DataPacketHeader() - packet_type = header.type_as_object - packet_type.ccd_side = ccd_side - packet_type.ccd_number = ccd_id - packet_type.last_packet = False - packet_type.frame_number = self._hk_data.frame_number - packet_type.mode = self._IN.ccd_mode_config - header.type = packet_type + Args: + data (ndarray): the full frame image data + header (DataPacketHeader): the data packet header + v_start (int): the first row to be transmitted + v_end (int): the last row to be transmitted + setup: Setup - LOGGER.info(f"**** {packet_type=!s}") - LOGGER.info(f"Sending data packets...{self.v_start=} {self.v_end=}") + Returns: - for packet in generate_data_packets(data, header, self.v_start, self.v_end): - self._transport.write_packet(packet.packet_as_bytes) - self._transport.flush() + """ + # steps: + # * reshape data to 1D array + # * update header with length, last_packet + # * increment sequence number? + # * convert data part into bytes object + # * concatenate header and data -> bytes + # * yield the packet + + N_FEE_SIDE = setup.camera.fee.ccd_sides.enum + + MAX_PACKET_SIZE = 32140 # this is a register value reg_4_config + HEADER_LENGTH = 10 + H_END = 2294 + MAX_CCD_LINE = 4509 + MAX_OVERSCAN_LINE = MAX_CCD_LINE + 30 + + nr_rows_in_packet = row_offset = (MAX_PACKET_SIZE - HEADER_LENGTH) // (H_END + 1) // 2 + + y_size, x_size = data.shape + h_end = x_size - 1 + v_end_ccd = min(MAX_CCD_LINE, v_end) + + ccd_side = header.type_as_object.ccd_side + + # F-side is read out starting from the right, so we flip the data left to right + # before sending, which simulates the reverse readout. + + data = np.fliplr(data) if ccd_side == N_FEE_SIDE.RIGHT_SIDE else data + + header.length = nr_rows_in_packet * ((h_end + 1) * 2) + LOGGER.debug(f"{header.length = }, {nr_rows_in_packet = }, {h_end = }") + + for idx in range(v_start, v_end_ccd + 1, nr_rows_in_packet): + if idx + nr_rows_in_packet > v_end_ccd: + row_offset = v_end_ccd - idx + 1 + header.length = row_offset * ((h_end + 1) * 2) + header.last_packet = True + # LOGGER.debug(f"{idx=}, {row_offset=}") + chunk = bytearray(data[idx:idx+row_offset, :]) + chunk[0::2], chunk[1::2] = chunk[1::2], chunk[0::2] + packet_data = header.data_as_bytes() + chunk + # LOGGER.debug(f"{len(packet_data)=}, {len(chunk)=}") + yield SpaceWirePacket.create_packet(packet_data) + + # reset the header for the overscan lines + + header.packet_type = PacketType.OVERSCAN_DATA + header.last_packet = False + header.length = nr_rows_in_packet * ((h_end + 1) * 2) + + v_end_overscan = min(MAX_OVERSCAN_LINE, v_end) + + # reset the row_offset + + row_offset = nr_rows_in_packet + + for idx in range(MAX_CCD_LINE+1, v_end_overscan + 1, nr_rows_in_packet): + if idx + nr_rows_in_packet > v_end_overscan: + row_offset = v_end_overscan - idx + 1 + header.length = row_offset * ((h_end + 1) * 2) + header.last_packet = True + LOGGER.debug(f"{idx=}, {row_offset=}") + chunk = bytearray(data[idx:idx+row_offset, :]) + chunk[0::2], chunk[1::2] = chunk[1::2], chunk[0::2] + packet_data = header.data_as_bytes() + chunk + LOGGER.debug(f"{len(packet_data)=}, {len(chunk)=}") + yield SpaceWirePacket.create_packet(packet_data) + + +def send_data_packets( + transport, timecode: int, ccd_id: int, ccd_side: int, frame_number: int, ccd_mode: int, + v_start: int, v_end: int, setup: Setup +): + """ + Generates pattern data and sends it over + """ + data = create_pattern_data(timecode, ccd_id, ccd_side) + + header = DataPacketHeader() + packet_type = header.type_as_object + packet_type.ccd_side = ccd_side + packet_type.ccd_number = ccd_id + packet_type.last_packet = False + packet_type.frame_number = frame_number + packet_type.mode = ccd_mode + header.type = packet_type + LOGGER.info(f"**** {packet_type=!s}") + LOGGER.info(f"Sending data packets...{v_start=} {v_end=}") + + for packet in generate_data_packets(data, header, v_start, v_end, setup): + if x := transport.write_packet(packet.packet_as_bytes): + LOGGER.error(f"Sending Data packet failed ({x}).") + transport.flush() -def process_rmap_command(transport: SpaceWireInterface, register: RegisterMap): + +def process_rmap_command(transport: SpaceWireInterface, register: RegisterMap, hk_data: HousekeepingData): _, buffer = transport.read_packet(timeout=200) if not buffer: @@ -617,39 +806,49 @@

    Module egse.fee.feesim

    # DPU Processor crashed or the connection dropped for some other reason. # We will receive one packet with 0 or 1 bytes. - if len(buffer) in (0, 1): + if len(buffer) in {0, 1}: return packet = SpaceWirePacket.create_packet(buffer) - LOGGER.info(f"SpW packet received: {packet}") + LOGGER.debug(f"Received {packet!r}") status = 0 # indicating OK if isinstance(packet, ReadRequest): - data = register.get_data(packet.address, packet.data_length) + # If address is start of HK memory area, read the data from the housekeeping data + if packet.address == 0x700: + data = hk_data.data_as_bytes() + else: + data = register.get_data(packet.address, packet.data_length) data = create_rmap_read_request_reply_packet( packet.instruction, packet.transaction_id, status, data, packet.data_length) - LOGGER.info(f"Sending a ReadRequestReply: {data[:10]=}") + LOGGER.debug(f"Sending {SpaceWirePacket.create_packet(data)!r}") transport.write_packet(data) transport.flush() elif isinstance(packet, WriteRequest): if packet.is_verified(): - LOGGER.info(f"Write data to register map: 0x{packet.address:x} {packet.data}") + LOGGER.debug( + f"Write data to register map: 0x{packet.address:x} [0x{' '.join(f'{x:02x}'for x in packet.data)}]" + ) register.set_data(packet.address, packet.data) data = create_rmap_write_request_reply_packet( packet.instruction, packet.transaction_id, status ) + LOGGER.debug(f"Sending {SpaceWirePacket.create_packet(data)!r}") + transport.write_packet(data) transport.flush() else: - LOGGER.warning(f"Unverified Write Request not yet implemented!") + LOGGER.warning("Unverified Write Request not yet implemented!") + else: + LOGGER.warning(f"Unexpected packet type received: {packet.__class__}") def start_fee_simulator(transport: SpaceWireInterface): @@ -660,7 +859,7 @@

    Module egse.fee.feesim

    except KeyboardInterrupt: print("Shutdown requested...exiting") except SystemExit as exit_code: - print("System Exit with code {}.".format(exit_code)) + print(f"System Exit with code {exit_code}.") return exit_code except Exception: import traceback @@ -693,6 +892,8 @@

    Module egse.fee.feesim

    When --tunnel is specified, DSI_ADDRESS will default to 127.0.0.1 unless explicitly provided. """ + multiprocessing.current_process().name = "feesim" + print(f"{tunnel=}, {zeromq=}, {ssh_user=}, {dsi_address=}, {dsi_port=}") if tunnel: @@ -764,6 +965,208 @@

    Module egse.fee.feesim

    Functions

    +
    +def create_pattern_data(timecode: int, ccd_id: int, ccd_side: int) ‑> numpy.ndarray +
    +
    +

    Create pattern data as a two-dimensional ND array. The pattern data is generated as described +in PLATO-LESIA-PL-TN-023 - SimuCam pattern requirement.

    +

    The pattern is build up like this: each pixel is a 16-bit number with the following structure:

    +
    * Bits [15:13] = time-code % 8
    +* Bits [12:11] = CCD number [1, 2, 3, 4]  should be [0-3]!
    +* Bit [10] = CCD side: 0 = left side, 1 = right side
    +* Bits [9:5] = Y-coordinate % 32
    +* Bits [4:0] = X-coordinate % 32
    +
    +

    The total image data size of a full frame CCD is:

    +
    x = 4590 = 2 x (25 [serial prescan] + 2255 + 15 [serial overscan])
    +y = 4540 = 4510 + 30 [parallel overscan]
    +
    +

    This function creates each side of the CCD separately, so each half can be treated individually +as is done in the N-FEE. The two sides can easily be concatenated to form the full image:

    +
    data = np.concatenate((data_left, data_right), axis=1)
    +
    +

    Args

    +
    +
    timecode : int
    +
    the timecode for this readout
    +
    ccd_id : int
    +
    the CCD number [0-3]
    +
    ccd_side : int
    +
    the CCD side, 0 = left = E-side, 1 = right = F-side
    +
    +

    Returns

    +

    A two-dimensional ND array representing half of a CCD.

    +
    + +Expand source code + +
    def create_pattern_data(timecode: int, ccd_id: int, ccd_side: int) -> np.ndarray:
    +    """
    +    Create pattern data as a two-dimensional ND array. The pattern data is generated as described
    +    in PLATO-LESIA-PL-TN-023 - SimuCam pattern requirement.
    +
    +    The pattern is build up like this: each pixel is a 16-bit number with the following structure:
    +
    +        * Bits [15:13] = time-code % 8
    +        * Bits [12:11] = CCD number [1, 2, 3, 4]  should be [0-3]!
    +        * Bit [10] = CCD side: 0 = left side, 1 = right side
    +        * Bits [9:5] = Y-coordinate % 32
    +        * Bits [4:0] = X-coordinate % 32
    +
    +    The total image data size of a full frame CCD is:
    +
    +        x = 4590 = 2 x (25 [serial prescan] + 2255 + 15 [serial overscan])
    +        y = 4540 = 4510 + 30 [parallel overscan]
    +
    +    This function creates each side of the CCD separately, so each half can be treated individually
    +    as is done in the N-FEE. The two sides can easily be concatenated to form the full image:
    +
    +        data = np.concatenate((data_left, data_right), axis=1)
    +
    +    Args:
    +        timecode (int): the timecode for this readout
    +        ccd_id (int): the CCD number [0-3]
    +        ccd_side (int): the CCD side, 0 = left = E-side, 1 = right = F-side
    +    Returns:
    +        A two-dimensional ND array representing half of a CCD.
    +    """
    +
    +    ccd_number = ccd_id  # save for later use
    +
    +    timecode = (timecode % 8) << 13  # timecode is 3 bits at [15:13]
    +    ccd_id = (ccd_id & 0b0011) << 11  # ccd_id is 2 bits at [12:11]
    +    ccd_side = (ccd_side & 0b0001) << 10  # ccd_side is 1 bit at [10]
    +
    +    x_size = 25 + 2255 + 15
    +    y_size = 4510 + 30
    +
    +    rows, cols = np.indices((y_size, x_size), dtype=np.uint16)
    +    cols %= 32
    +    rows %= 32
    +
    +    data = rows * 16 + cols + timecode + ccd_side + ccd_id
    +
    +    # We leave set the msb because of the bit flipt for N-FEE EM
    +
    +    data[50:300, 100:105] = ccd_number | 0b10000000_00000000
    +    data[100:105, 50:500] = ccd_number | 0b10000000_00000000
    +    data[300:305, 50:150] = ccd_number | 0b10000000_00000000
    +    data[50:150, 500:505] = ccd_number | 0b10000000_00000000
    +
    +    # We unset the msb because of the bit flip for N-FEE EM
    +
    +    data[110, 110] = 0x7FFF
    +
    +    return data
    +
    +
    +
    +def generate_data_packets(data: numpy.ndarray, header: DataPacketHeader, v_start: int, v_end: int, setup: Setup) +
    +
    +

    This generator function creates and returns the SpaceWire packets to send to the DPU Processor.

    +

    Args

    +
    +
    data : ndarray
    +
    the full frame image data
    +
    header : DataPacketHeader
    +
    the data packet header
    +
    v_start : int
    +
    the first row to be transmitted
    +
    v_end : int
    +
    the last row to be transmitted
    +
    setup
    +
    Setup
    +
    +

    Returns:

    +
    + +Expand source code + +
    def generate_data_packets(data: np.ndarray, header: DataPacketHeader, v_start: int, v_end: int, setup: Setup):
    +    """
    +    This generator function creates and returns the SpaceWire packets to send to the DPU Processor.
    +
    +    Args:
    +        data (ndarray): the full frame image data
    +        header (DataPacketHeader): the data packet header
    +        v_start (int): the first row to be transmitted
    +        v_end (int): the last row to be transmitted
    +        setup: Setup
    +
    +    Returns:
    +
    +    """
    +    # steps:
    +    # * reshape data to 1D array
    +    # * update header with length, last_packet
    +    # * increment sequence number?
    +    # * convert data part into bytes object
    +    # * concatenate header and data -> bytes
    +    # * yield the packet
    +
    +    N_FEE_SIDE = setup.camera.fee.ccd_sides.enum
    +
    +    MAX_PACKET_SIZE = 32140  # this is a register value reg_4_config
    +    HEADER_LENGTH = 10
    +    H_END = 2294
    +    MAX_CCD_LINE = 4509
    +    MAX_OVERSCAN_LINE = MAX_CCD_LINE + 30
    +
    +    nr_rows_in_packet = row_offset = (MAX_PACKET_SIZE - HEADER_LENGTH) // (H_END + 1) // 2
    +
    +    y_size, x_size = data.shape
    +    h_end = x_size - 1
    +    v_end_ccd = min(MAX_CCD_LINE, v_end)
    +
    +    ccd_side = header.type_as_object.ccd_side
    +
    +    # F-side is read out starting from the right, so we flip the data left to right
    +    # before sending, which simulates the reverse readout.
    +
    +    data = np.fliplr(data) if ccd_side == N_FEE_SIDE.RIGHT_SIDE else data
    +
    +    header.length = nr_rows_in_packet * ((h_end + 1) * 2)
    +    LOGGER.debug(f"{header.length = }, {nr_rows_in_packet = }, {h_end = }")
    +
    +    for idx in range(v_start, v_end_ccd + 1, nr_rows_in_packet):
    +        if idx + nr_rows_in_packet > v_end_ccd:
    +            row_offset = v_end_ccd - idx + 1
    +            header.length = row_offset * ((h_end + 1) * 2)
    +            header.last_packet = True
    +        # LOGGER.debug(f"{idx=}, {row_offset=}")
    +        chunk = bytearray(data[idx:idx+row_offset, :])
    +        chunk[0::2], chunk[1::2] = chunk[1::2], chunk[0::2]
    +        packet_data = header.data_as_bytes() + chunk
    +        # LOGGER.debug(f"{len(packet_data)=}, {len(chunk)=}")
    +        yield SpaceWirePacket.create_packet(packet_data)
    +
    +    # reset the header for the overscan lines
    +
    +    header.packet_type = PacketType.OVERSCAN_DATA
    +    header.last_packet = False
    +    header.length = nr_rows_in_packet * ((h_end + 1) * 2)
    +
    +    v_end_overscan = min(MAX_OVERSCAN_LINE, v_end)
    +
    +    # reset the row_offset
    +
    +    row_offset = nr_rows_in_packet
    +
    +    for idx in range(MAX_CCD_LINE+1, v_end_overscan + 1, nr_rows_in_packet):
    +        if idx + nr_rows_in_packet > v_end_overscan:
    +            row_offset = v_end_overscan - idx + 1
    +            header.length = row_offset * ((h_end + 1) * 2)
    +            header.last_packet = True
    +        LOGGER.debug(f"{idx=}, {row_offset=}")
    +        chunk = bytearray(data[idx:idx+row_offset, :])
    +        chunk[0::2], chunk[1::2] = chunk[1::2], chunk[0::2]
    +        packet_data = header.data_as_bytes() + chunk
    +        LOGGER.debug(f"{len(packet_data)=}, {len(chunk)=}")
    +        yield SpaceWirePacket.create_packet(packet_data)
    +
    +
    def goto_on_mode(state)
    @@ -829,8 +1232,155 @@

    Functions

    return states.ST_WINDOWING
    +
    +def initialise_hk_data(hk_data: HousekeepingData) ‑> HousekeepingData +
    +
    +

    Initialises the housekeeping data to fake or simulated values.

    +
    + +Expand source code + +
    def initialise_hk_data(hk_data: HousekeepingData) -> HousekeepingData:
    +    """Initialises the housekeeping data to fake or simulated values."""
    +
    +    try:
    +        setup = load_setup()
    +        hk_info_table = setup.telemetry.dictionary
    +    except AttributeError as exc:
    +        raise SetupError("Version of the telemetry dictionary not specified in the current setup.") from exc
    +
    +    storage_mnemonic_col = hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values
    +    original_name_col = hk_info_table[TmDictionaryColumns.ORIGINAL_EGSE_HK_NAMES].values
    +    selection = np.where(storage_mnemonic_col == ORIGIN)
    +    original_names = original_name_col[selection]
    +
    +    for name in temperature_walks:
    +        hk_data[name] = int(next(temperature_walks[name]))
    +
    +    hk_data["CCD1_TS"] = 0x7FFF
    +    hk_data["CCD2_TS"] = 0x7FFF
    +    hk_data["CCD3_TS"] = 0x7FFF
    +    hk_data["CCD4_TS"] = 0x7FFF
    +
    +    hk_data["PRT1"] = 0x7FFF
    +    hk_data["PRT2"] = 0x7FFF
    +    hk_data["PRT3"] = 0x7FFF
    +    hk_data["PRT4"] = 0x7FFF
    +    hk_data["PRT5"] = 0x7FFF
    +
    +    hk_data["ZERO_DIFF_AMP"] = 0x8015
    +
    +    if "CCD1_VOD_MON" in original_names:
    +        hk_data["CCD1_VOD_MON"] = 0x8055
    +    else:
    +        if "CCD1_VOD_MON_E" in original_names:
    +            hk_data["CCD1_VOD_MON_E"] = 0x8055
    +        if "CCD1_VOD_MON_F" in original_names:
    +            hk_data["CCD1_VOD_MON_F"] = 0x8055
    +    hk_data["CCD1_VOG_MON"] = 0x8056
    +    hk_data["CCD1_VRD_MON_E"] = 0x8056
    +
    +    if "CCD2_VOD_MON" in original_names:
    +        hk_data["CCD2_VOD_MON"] = 0x8057
    +    else:
    +        if "CCD2_VOD_MON_E" in original_names:
    +            hk_data["CCD2_VOD_MON_E"] = 0x8057
    +        if "CCD2_VOD_MON_F" in original_names:
    +            hk_data["CCD2_VOD_MON_F"] = 0x8057
    +    hk_data["CCD2_VOG_MON"] = 0x8058
    +    hk_data["CCD2_VRD_MON_E"] = 0x8057
    +
    +    if "CCD3_VOD_MON" in original_names:
    +        hk_data["CCD3_VOD_MON"] = 0x8058
    +    else:
    +        if "CCD3_VOD_MON_E" in original_names:
    +            hk_data["CCD3_VOD_MON_E"] = 0x8058
    +        if "CCD3_VOD_MON_F" in original_names:
    +            hk_data["CCD3_VOD_MON_F"] = 0x8058
    +    hk_data["CCD3_VOG_MON"] = 0x8058
    +    hk_data["CCD3_VRD_MON_E"] = 0x8058
    +
    +    if "CCD4_VOD_MON" in original_names:
    +        hk_data["CCD4_VOD_MON"] = 0x8057
    +    else:
    +        if "CCD4_VOD_MON_E" in original_names:
    +            hk_data["CCD4_VOD_MON_E"] = 0x8057
    +        if "CCD4_VOD_MON_F" in original_names:
    +            hk_data["CCD4_VOD_MON_F"] = 0x8057
    +    hk_data["CCD4_VOG_MON"] = 0x8058
    +    hk_data["CCD4_VRD_MON_E"] = 0x8058
    +
    +    hk_data["VCCD"] = 0x39BF
    +    hk_data["VRCLK_MON"] = 0xFC8A
    +    hk_data["VICLK"] = 0xFAE9
    +    if "VRCLK_LOW" in original_names:
    +        hk_data["VRCLK_LOW"] = 0x821A
    +
    +    if "5VB_POS_MON" in original_names:
    +        hk_data["5VB_POS_MON"] = 0x1E6A
    +    hk_data["5VB_NEG_MON"] = 0x1A9F
    +    hk_data["3V3B_MON"] = 0xE75D
    +    hk_data["2V5A_MON"] = 0x1979
    +    hk_data["3V3D_MON"] = 0xE76E
    +    hk_data["2V5D_MON"] = 0x1A8C
    +    hk_data["1V5D_MON"] = 0xDF35
    +    hk_data["5VREF_MON"] = 0x1A80
    +
    +    hk_data["VCCD_POS_RAW"] = 0x53BF
    +    hk_data["VCLK_POS_RAW"] = 0x40BA
    +    hk_data["VAN1_POS_RAW"] = 0x0744
    +    hk_data["VAN3_NEG_MON"] = 0xFB7C
    +    hk_data["VAN2_POS_RAW"] = 0x3AEC
    +    hk_data["VDIG_RAW"] = 0x0AB5
    +    if "5VB_POS_MON" in original_names:
    +        hk_data["VDIG_RAW_2"] = 0x0A32
    +    if "VICLK_LOW" in original_names:
    +        hk_data["VICLK_LOW"] = 0x8277
    +
    +    hk_data["CCD1_VRD_MON_F"] = 0x8059
    +    hk_data["CCD1_VDD_MON"] = 0x94CA
    +    hk_data["CCD1_VGD_MON"] = 0x8056
    +    hk_data["CCD2_VRD_MON_F"] = 0x8058
    +    hk_data["CCD2_VDD_MON"] = 0x94C1
    +    hk_data["CCD2_VGD_MON"] = 0x8055
    +    hk_data["CCD3_VRD_MON_F"] = 0x8059
    +    hk_data["CCD3_VDD_MON"] = 0x94C1
    +    hk_data["CCD3_VGD_MON"] = 0x8058
    +    hk_data["CCD4_VRD_MON_F"] = 0x8058
    +    hk_data["CCD4_VDD_MON"] = 0x94BA
    +    hk_data["CCD4_VGD_MON"] = 0x8056
    +
    +    hk_data["IG_HI_MON"] = 0x8057
    +    if "IG_LO_MON" in original_names:
    +        hk_data["IG_LO_MON"] = 0x8059
    +    hk_data["TSENSE_A"] = 0x8059
    +    hk_data["TSENSE_B"] = 0x805A
    +
    +    hk_data["spw_timecode"] = 0x0000
    +    hk_data["rmap_target_status"] = 0x0000
    +    hk_data["rmap_target_indicate"] = 0x0000
    +    hk_data["spw_link_escape_error"] = 0x0000
    +    hk_data["spw_credit_error"] = 0x0000
    +    hk_data["spw_parity_error"] = 0x0000
    +    hk_data["spw_link_disconnect"] = 0x0000
    +    hk_data["spw_link_running"] = 0x0001
    +
    +    hk_data["frame_counter"] = 0x0000
    +    hk_data["op_mode"] = 0x0000
    +    hk_data["frame_number"] = 0x0000
    +
    +    hk_data["error_flags"] = 0x0000
    +
    +    hk_data["FPGA minor version"] = 0x0018
    +    hk_data["FPGA major version"] = 0x0000
    +    hk_data["Board ID"] = 0x0000
    +
    +    return hk_data
    +
    +
    -def process_rmap_command(transport: SpaceWireInterface, register: RegisterMap) +def process_rmap_command(transport: SpaceWireInterface, register: RegisterMap, hk_data: HousekeepingData)
    @@ -838,7 +1388,7 @@

    Functions

    Expand source code -
    def process_rmap_command(transport: SpaceWireInterface, register: RegisterMap):
    +
    def process_rmap_command(transport: SpaceWireInterface, register: RegisterMap, hk_data: HousekeepingData):
     
         _, buffer = transport.read_packet(timeout=200)
         if not buffer:
    @@ -848,39 +1398,85 @@ 

    Functions

    # DPU Processor crashed or the connection dropped for some other reason. # We will receive one packet with 0 or 1 bytes. - if len(buffer) in (0, 1): + if len(buffer) in {0, 1}: return packet = SpaceWirePacket.create_packet(buffer) - LOGGER.info(f"SpW packet received: {packet}") + LOGGER.debug(f"Received {packet!r}") status = 0 # indicating OK if isinstance(packet, ReadRequest): - data = register.get_data(packet.address, packet.data_length) + # If address is start of HK memory area, read the data from the housekeeping data + if packet.address == 0x700: + data = hk_data.data_as_bytes() + else: + data = register.get_data(packet.address, packet.data_length) data = create_rmap_read_request_reply_packet( packet.instruction, packet.transaction_id, status, data, packet.data_length) - LOGGER.info(f"Sending a ReadRequestReply: {data[:10]=}") + LOGGER.debug(f"Sending {SpaceWirePacket.create_packet(data)!r}") transport.write_packet(data) transport.flush() elif isinstance(packet, WriteRequest): if packet.is_verified(): - LOGGER.info(f"Write data to register map: 0x{packet.address:x} {packet.data}") + LOGGER.debug( + f"Write data to register map: 0x{packet.address:x} [0x{' '.join(f'{x:02x}'for x in packet.data)}]" + ) register.set_data(packet.address, packet.data) data = create_rmap_write_request_reply_packet( packet.instruction, packet.transaction_id, status ) + LOGGER.debug(f"Sending {SpaceWirePacket.create_packet(data)!r}") + transport.write_packet(data) transport.flush() else: - LOGGER.warning(f"Unverified Write Request not yet implemented!")
    + LOGGER.warning("Unverified Write Request not yet implemented!") + else: + LOGGER.warning(f"Unexpected packet type received: {packet.__class__}")
    + +
    +
    +def send_data_packets(transport, timecode: int, ccd_id: int, ccd_side: int, frame_number: int, ccd_mode: int, v_start: int, v_end: int, setup: Setup) +
    +
    +

    Generates pattern data and sends it over

    +
    + +Expand source code + +
    def send_data_packets(
    +        transport, timecode: int, ccd_id: int, ccd_side: int, frame_number: int, ccd_mode: int,
    +        v_start: int, v_end: int, setup: Setup
    +):
    +        """
    +        Generates pattern data and sends it over
    +        """
    +        data = create_pattern_data(timecode, ccd_id, ccd_side)
    +
    +        header = DataPacketHeader()
    +        packet_type = header.type_as_object
    +        packet_type.ccd_side = ccd_side
    +        packet_type.ccd_number = ccd_id
    +        packet_type.last_packet = False
    +        packet_type.frame_number = frame_number
    +        packet_type.mode = ccd_mode
    +        header.type = packet_type
    +
    +        LOGGER.info(f"**** {packet_type=!s}")
    +        LOGGER.info(f"Sending data packets...{v_start=} {v_end=}")
    +
    +        for packet in generate_data_packets(data, header, v_start, v_end, setup):
    +            if x := transport.write_packet(packet.packet_as_bytes):
    +                LOGGER.error(f"Sending Data packet failed ({x}).")
    +            transport.flush()
    @@ -900,7 +1496,7 @@

    Functions

    except KeyboardInterrupt: print("Shutdown requested...exiting") except SystemExit as exit_code: - print("System Exit with code {}.".format(exit_code)) + print(f"System Exit with code {exit_code}.") return exit_code except Exception: import traceback @@ -943,148 +1539,15 @@

    Args

    self._hk_data = HousekeepingData() self._hk_header = DataPacketHeader() + self.setup = load_setup() + self._IN = NFEEInternals(self.register_map) - self.initialise_hk_data() + initialise_hk_data(self._hk_data) self.initialise_hk_header() - def initialise_hk_data(self): - """Initialises the housekeeping data to fake or simulated values.""" - - try: - hk_info_table = GlobalState.setup.telemetry.dictionary - except AttributeError: - raise SetupError("Version of the telemetry dictionary not specified in the current setup") - - storage_mnemonic_col = hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values - original_name_col = hk_info_table[TmDictionaryColumns.ORIGINAL_EGSE_HK_NAMES].values - selection = np.where(storage_mnemonic_col == ORIGIN) - original_names = original_name_col[selection] - - self._hk_data["TOU_SENSE_1"] = 0x8000 - self._hk_data["TOU_SENSE_2"] = 0x8000 - self._hk_data["TOU_SENSE_3"] = 0x8000 - self._hk_data["TOU_SENSE_4"] = 0x8000 - self._hk_data["TOU_SENSE_5"] = 0x8000 - self._hk_data["TOU_SENSE_6"] = 0x8000 - - self._hk_data["CCD1_TS"] = 0x7FFF - self._hk_data["CCD2_TS"] = 0x7FFF - self._hk_data["CCD3_TS"] = 0x7FFF - self._hk_data["CCD4_TS"] = 0x7FFF - - self._hk_data["PRT1"] = 0x7FFF - self._hk_data["PRT2"] = 0x7FFF - self._hk_data["PRT3"] = 0x7FFF - self._hk_data["PRT4"] = 0x7FFF - self._hk_data["PRT5"] = 0x7FFF - - self._hk_data["ZERO_DIFF_AMP"] = 0x8015 - - if "CCD1_VOD_MON" in original_names: - self._hk_data["CCD1_VOD_MON"] = 0x8055 - else: - if "CCD1_VOD_MON_E" in original_names: - self._hk_data["CCD1_VOD_MON_E"] = 0x8055 - if "CCD1_VOD_MON_F" in original_names: - self._hk_data["CCD1_VOD_MON_F"] = 0x8055 - self._hk_data["CCD1_VOG_MON"] = 0x8056 - self._hk_data["CCD1_VRD_MON_E"] = 0x8056 - - if "CCD2_VOD_MON" in original_names: - self._hk_data["CCD2_VOD_MON"] = 0x8057 - else: - if "CCD2_VOD_MON_E" in original_names: - self._hk_data["CCD2_VOD_MON_E"] = 0x8057 - if "CCD2_VOD_MON_E" in original_names: - self._hk_data["CCD2_VOD_MON_E"] = 0x8057 - self._hk_data["CCD2_VOG_MON"] = 0x8058 - self._hk_data["CCD2_VRD_MON_E"] = 0x8057 - - if "CCD3_VOD_MON" in original_names: - self._hk_data["CCD3_VOD_MON"] = 0x8058 - else: - if "CCD3_VOD_MON_E" in original_names: - self._hk_data["CCD3_VOD_MON_E"] = 0x8058 - if "CCD3_VOD_MON_F" in original_names: - self._hk_data["CCD3_VOD_MON_F"] = 0x8058 - self._hk_data["CCD3_VOG_MON"] = 0x8058 - self._hk_data["CCD3_VRD_MON_E"] = 0x8058 - - if "CCD4_VOD_MON" in original_names: - self._hk_data["CCD4_VOD_MON"] = 0x8057 - else: - if "CCD4_VOD_MON_E" in original_names: - self._hk_data["CCD4_VOD_MON_E"] = 0x8057 - if "CCD4_VOD_MON_F" in original_names: - self._hk_data["CCD4_VOD_MON_F"] = 0x8057 - self._hk_data["CCD4_VOG_MON"] = 0x8058 - self._hk_data["CCD4_VRD_MON_E"] = 0x8058 - - self._hk_data["VCCD"] = 0x39BF - self._hk_data["VRCLK_MON"] = 0xFC8A - self._hk_data["VICLK"] = 0xFAE9 - if "VRCLK_LOW" in original_names: - self._hk_data["VRCLK_LOW"] = 0x821A - - if "5VB_POS_MON" in original_names: - self._hk_data["5VB_POS_MON"] = 0x1E6A - self._hk_data["5VB_NEG_MON"] = 0x1A9F - self._hk_data["3V3B_MON"] = 0xE75D - self._hk_data["2V5A_MON"] = 0x1979 - self._hk_data["3V3D_MON"] = 0xE76E - self._hk_data["2V5D_MON"] = 0x1A8C - self._hk_data["1V5D_MON"] = 0xDF35 - self._hk_data["5VREF_MON"] = 0x1A80 - - self._hk_data["VCCD_POS_RAW"] = 0x53BF - self._hk_data["VCLK_POS_RAW"] = 0x40BA - self._hk_data["VAN1_POS_RAW"] = 0x0744 - self._hk_data["VAN3_NEG_MON"] = 0xFB7C - self._hk_data["VAN2_POS_RAW"] = 0x3AEC - self._hk_data["VDIG_RAW"] = 0x0AB5 - if "5VB_POS_MON" in original_names: - self._hk_data["VDIG_RAW_2"] = 0x0A32 - if "VICLK_LOW" in original_names: - self._hk_data["VICLK_LOW"] = 0x8277 - - self._hk_data["CCD1_VRD_MON_F"] = 0x8059 - self._hk_data["CCD1_VDD_MON"] = 0x94CA - self._hk_data["CCD1_VGD_MON"] = 0x8056 - self._hk_data["CCD2_VRD_MON_F"] = 0x8058 - self._hk_data["CCD2_VDD_MON"] = 0x94C1 - self._hk_data["CCD2_VGD_MON"] = 0x8055 - self._hk_data["CCD3_VRD_MON_F"] = 0x8059 - self._hk_data["CCD3_VDD_MON"] = 0x94C1 - self._hk_data["CCD3_VGD_MON"] = 0x8058 - self._hk_data["CCD4_VRD_MON_F"] = 0x8058 - self._hk_data["CCD4_VDD_MON"] = 0x94BA - self._hk_data["CCD4_VGD_MON"] = 0x8056 - - self._hk_data["IG_HI_MON"] = 0x8057 - if "IG_LO_MON" in original_names: - self._hk_data["IG_LO_MON"] = 0x8059 - self._hk_data["TSENSE_A"] = 0x8059 - self._hk_data["TSENSE_B"] = 0x805A - - self._hk_data["spw_timecode"] = 0x0000 - self._hk_data["rmap_target_status"] = 0x0000 - self._hk_data["rmap_target_indicate"] = 0x0000 - self._hk_data["spw_link_escape_error"] = 0x0000 - self._hk_data["spw_credit_error"] = 0x0000 - self._hk_data["spw_parity_error"] = 0x0000 - self._hk_data["spw_link_disconnect"] = 0x0000 - self._hk_data["spw_link_running"] = 0x0001 - - self._hk_data["frame_counter"] = 0x0000 - self._hk_data["op_mode"] = 0x0000 - self._hk_data["frame_number"] = 0x0000 - - self._hk_data["error_flags"] = 0x0000 - - self._hk_data["FPGA minor version"] = 0x0018 - self._hk_data["FPGA major version"] = 0x0000 - self._hk_data["Board ID"] = 0x0000 + self._killer = SignalCatcher() + def initialise_hk_header(self): """ @@ -1149,8 +1612,8 @@

    Args

    def run(self): LOGGER.info(f"FEE Simulator Current State: {self.decode_current_state()}") - n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum - sensor_sel_enum = GlobalState.setup.camera.fee.sensor_sel.enum + n_fee_side = self.setup.camera.fee.ccd_sides.enum + sensor_sel_enum = self.setup.camera.fee.sensor_sel.enum with self._transport: @@ -1168,15 +1631,21 @@

    Args

    sync_period = self.get_sync_period() + if self._killer.term_signal_received: + LOGGER.warning(f"Terminating N-FEE Simulator after receiving {self._killer.signal_name}") + break + if time.perf_counter() <= start_time + sync_period: continue + # LOGGER.debug(f"{'-'*80} {sync_period = }") + start_time = time.perf_counter() # Send a timecode packet ----------------------------------------------------------- timecode = self._hk_data.timecode - LOGGER.info(f"Sending timecode=0x{timecode:02X}") + LOGGER.info(f"Sending timecode: 0x{timecode:02X} ({timecode})") self._transport.send_timecode(timecode) # Send a housekeeping packet ------------------------------------------------------- @@ -1184,7 +1653,7 @@

    Args

    LOGGER.info(f"Sending HK packet: frame_counter={self._hk_data.frame_counter}, " f"sequence_counter={self._hk_header.sequence_counter}, " f"type={self._hk_header.type_as_object}") - LOGGER.info(f"error_flags={self._hk_data['error_flags']}") + LOGGER.debug(f"HK Packet: error_flags=0b{self._hk_data['error_flags']:0b}") data = self._hk_header.data_as_bytes() + self._hk_data.data_as_bytes() packet = SpaceWirePacket.create_packet(data) @@ -1192,6 +1661,13 @@

    Args

    self._transport.flush() + # Check if error flags must be cleared --------------------------------------------- + + if self.register_map['clear_error_flag']: + LOGGER.info("Clearing error flags") + self.register_map[('reg_21_config', 'clear_error_flag')] = 0 + self._hk_data['error_flags'] = 0 + # Send the Data packets ------------------------------------------------------------ LOGGER.info(f"mode={self.decode_current_state()}, " @@ -1207,25 +1683,39 @@

    Args

    n_fee_mode.FULL_IMAGE_MODE,)): try: - ccd_id_to_bin = GlobalState.setup.camera.fee.ccd_numbering.CCD_ID_TO_BIN + ccd_id_to_bin = self.setup.camera.fee.ccd_numbering.CCD_ID_TO_BIN except AttributeError: raise SetupError("No entry in the setup for camera.fee.ccd_numbering.CCD_ID_TO_BIN") + LOGGER.info("Sending Data packets...") + ccd_number = ccd_id_to_bin[self.get_current_ccd_number()] # N-FEE CCD number [0-3] if self.sensor_sel & sensor_sel_enum.E_SIDE: - self.send_data_packets(timecode, ccd_id=ccd_number, ccd_side=n_fee_side.E_SIDE) + send_data_packets( + self._transport, timecode, + ccd_id=ccd_number, ccd_side=n_fee_side.E_SIDE, frame_number=self._hk_data.frame_number, + ccd_mode=self._IN.ccd_mode_config, v_start=self.v_start, v_end=self.v_end, setup=self.setup + ) if self.sensor_sel & sensor_sel_enum.F_SIDE: - self.send_data_packets(timecode, ccd_id=ccd_number, ccd_side=n_fee_side.F_SIDE) + send_data_packets( + self._transport, timecode, + ccd_id=ccd_number, ccd_side=n_fee_side.F_SIDE, frame_number=self._hk_data.frame_number, + ccd_mode=self._IN.ccd_mode_config, v_start=self.v_start, v_end=self.v_end, setup=self.setup + ) + + if self._killer.user_signal_received and self._killer.signal_name == "SIGUSR1": + # On USR1 signal, set the error flags to a random number. There are currently 12 error flags. + self.set_error_flags(random.randint(0x0, 0b1111_1111_1111)) + self._killer.clear() # Process any RMAP commands that were sent ----------------------------------------- - LOGGER.info( - f"Calling process_rmap_commands() method for " - f"{self._transport.__class__.__name__}." - ) + LOGGER.info("Processing RMAP commands...") while time.perf_counter() < start_time + sync_period - 0.2: - process_rmap_command(self._transport, self.register_map) + process_rmap_command(self._transport, self.register_map, self._hk_data) + + LOGGER.info("Updating internals...") self._hk_data.increment_timecode() @@ -1240,31 +1730,11 @@

    Args

    self._IN.update(200) self.update_hk_header(200) - self.update_error_flags() - - def update_error_flags(self): - """Set error_flags to 0b0000_1000_1000 (136) whenever the frame counter is a multiple of 5.""" - self._hk_data["error_flags"] = 0b0000_0000_0000 if self._hk_data.frame_counter % 5 else 0b0000_1000_1000 - - def send_data_packets(self, timecode: int, ccd_id: int, ccd_side: int): - - data = create_pattern_data(timecode, ccd_id, ccd_side) - - header = DataPacketHeader() - packet_type = header.type_as_object - packet_type.ccd_side = ccd_side - packet_type.ccd_number = ccd_id - packet_type.last_packet = False - packet_type.frame_number = self._hk_data.frame_number - packet_type.mode = self._IN.ccd_mode_config - header.type = packet_type - LOGGER.info(f"**** {packet_type=!s}") - LOGGER.info(f"Sending data packets...{self.v_start=} {self.v_end=}") - - for packet in generate_data_packets(data, header, self.v_start, self.v_end): - self._transport.write_packet(packet.packet_as_bytes) - self._transport.flush()
    + def set_error_flags(self, flags: int): + """Set error_flags to the given number.""" + LOGGER.warning(f"Setting error flags to 0b{flags:012b}") + self._hk_data["error_flags"] = flags

    Instance variables

    @@ -1377,154 +1847,6 @@

    Methods

    return self._IN.get_sync_period()
    -
    -def initialise_hk_data(self) -
    -
    -

    Initialises the housekeeping data to fake or simulated values.

    -
    - -Expand source code - -
    def initialise_hk_data(self):
    -    """Initialises the housekeeping data to fake or simulated values."""
    -
    -    try:
    -        hk_info_table = GlobalState.setup.telemetry.dictionary
    -    except AttributeError:
    -        raise SetupError("Version of the telemetry dictionary not specified in the current setup")
    -
    -    storage_mnemonic_col = hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values
    -    original_name_col = hk_info_table[TmDictionaryColumns.ORIGINAL_EGSE_HK_NAMES].values
    -    selection = np.where(storage_mnemonic_col == ORIGIN)
    -    original_names = original_name_col[selection]
    -
    -    self._hk_data["TOU_SENSE_1"] = 0x8000
    -    self._hk_data["TOU_SENSE_2"] = 0x8000
    -    self._hk_data["TOU_SENSE_3"] = 0x8000
    -    self._hk_data["TOU_SENSE_4"] = 0x8000
    -    self._hk_data["TOU_SENSE_5"] = 0x8000
    -    self._hk_data["TOU_SENSE_6"] = 0x8000
    -
    -    self._hk_data["CCD1_TS"] = 0x7FFF
    -    self._hk_data["CCD2_TS"] = 0x7FFF
    -    self._hk_data["CCD3_TS"] = 0x7FFF
    -    self._hk_data["CCD4_TS"] = 0x7FFF
    -
    -    self._hk_data["PRT1"] = 0x7FFF
    -    self._hk_data["PRT2"] = 0x7FFF
    -    self._hk_data["PRT3"] = 0x7FFF
    -    self._hk_data["PRT4"] = 0x7FFF
    -    self._hk_data["PRT5"] = 0x7FFF
    -
    -    self._hk_data["ZERO_DIFF_AMP"] = 0x8015
    -
    -    if "CCD1_VOD_MON" in original_names:
    -        self._hk_data["CCD1_VOD_MON"] = 0x8055
    -    else:
    -        if "CCD1_VOD_MON_E" in original_names:
    -            self._hk_data["CCD1_VOD_MON_E"] = 0x8055
    -        if "CCD1_VOD_MON_F" in original_names:
    -            self._hk_data["CCD1_VOD_MON_F"] = 0x8055
    -    self._hk_data["CCD1_VOG_MON"] = 0x8056
    -    self._hk_data["CCD1_VRD_MON_E"] = 0x8056
    -
    -    if "CCD2_VOD_MON" in original_names:
    -        self._hk_data["CCD2_VOD_MON"] = 0x8057
    -    else:
    -        if "CCD2_VOD_MON_E" in original_names:
    -            self._hk_data["CCD2_VOD_MON_E"] = 0x8057
    -        if "CCD2_VOD_MON_E" in original_names:
    -            self._hk_data["CCD2_VOD_MON_E"] = 0x8057
    -    self._hk_data["CCD2_VOG_MON"] = 0x8058
    -    self._hk_data["CCD2_VRD_MON_E"] = 0x8057
    -
    -    if "CCD3_VOD_MON" in original_names:
    -        self._hk_data["CCD3_VOD_MON"] = 0x8058
    -    else:
    -        if "CCD3_VOD_MON_E" in original_names:
    -            self._hk_data["CCD3_VOD_MON_E"] = 0x8058
    -        if "CCD3_VOD_MON_F" in original_names:
    -            self._hk_data["CCD3_VOD_MON_F"] = 0x8058
    -    self._hk_data["CCD3_VOG_MON"] = 0x8058
    -    self._hk_data["CCD3_VRD_MON_E"] = 0x8058
    -
    -    if "CCD4_VOD_MON" in original_names:
    -        self._hk_data["CCD4_VOD_MON"] = 0x8057
    -    else:
    -        if "CCD4_VOD_MON_E" in original_names:
    -            self._hk_data["CCD4_VOD_MON_E"] = 0x8057
    -        if "CCD4_VOD_MON_F" in original_names:
    -            self._hk_data["CCD4_VOD_MON_F"] = 0x8057
    -    self._hk_data["CCD4_VOG_MON"] = 0x8058
    -    self._hk_data["CCD4_VRD_MON_E"] = 0x8058
    -
    -    self._hk_data["VCCD"] = 0x39BF
    -    self._hk_data["VRCLK_MON"] = 0xFC8A
    -    self._hk_data["VICLK"] = 0xFAE9
    -    if "VRCLK_LOW" in original_names:
    -        self._hk_data["VRCLK_LOW"] = 0x821A
    -
    -    if "5VB_POS_MON" in original_names:
    -        self._hk_data["5VB_POS_MON"] = 0x1E6A
    -    self._hk_data["5VB_NEG_MON"] = 0x1A9F
    -    self._hk_data["3V3B_MON"] = 0xE75D
    -    self._hk_data["2V5A_MON"] = 0x1979
    -    self._hk_data["3V3D_MON"] = 0xE76E
    -    self._hk_data["2V5D_MON"] = 0x1A8C
    -    self._hk_data["1V5D_MON"] = 0xDF35
    -    self._hk_data["5VREF_MON"] = 0x1A80
    -
    -    self._hk_data["VCCD_POS_RAW"] = 0x53BF
    -    self._hk_data["VCLK_POS_RAW"] = 0x40BA
    -    self._hk_data["VAN1_POS_RAW"] = 0x0744
    -    self._hk_data["VAN3_NEG_MON"] = 0xFB7C
    -    self._hk_data["VAN2_POS_RAW"] = 0x3AEC
    -    self._hk_data["VDIG_RAW"] = 0x0AB5
    -    if "5VB_POS_MON" in original_names:
    -        self._hk_data["VDIG_RAW_2"] = 0x0A32
    -    if "VICLK_LOW" in original_names:
    -        self._hk_data["VICLK_LOW"] = 0x8277
    -
    -    self._hk_data["CCD1_VRD_MON_F"] = 0x8059
    -    self._hk_data["CCD1_VDD_MON"] = 0x94CA
    -    self._hk_data["CCD1_VGD_MON"] = 0x8056
    -    self._hk_data["CCD2_VRD_MON_F"] = 0x8058
    -    self._hk_data["CCD2_VDD_MON"] = 0x94C1
    -    self._hk_data["CCD2_VGD_MON"] = 0x8055
    -    self._hk_data["CCD3_VRD_MON_F"] = 0x8059
    -    self._hk_data["CCD3_VDD_MON"] = 0x94C1
    -    self._hk_data["CCD3_VGD_MON"] = 0x8058
    -    self._hk_data["CCD4_VRD_MON_F"] = 0x8058
    -    self._hk_data["CCD4_VDD_MON"] = 0x94BA
    -    self._hk_data["CCD4_VGD_MON"] = 0x8056
    -
    -    self._hk_data["IG_HI_MON"] = 0x8057
    -    if "IG_LO_MON" in original_names:
    -        self._hk_data["IG_LO_MON"] = 0x8059
    -    self._hk_data["TSENSE_A"] = 0x8059
    -    self._hk_data["TSENSE_B"] = 0x805A
    -
    -    self._hk_data["spw_timecode"] = 0x0000
    -    self._hk_data["rmap_target_status"] = 0x0000
    -    self._hk_data["rmap_target_indicate"] = 0x0000
    -    self._hk_data["spw_link_escape_error"] = 0x0000
    -    self._hk_data["spw_credit_error"] = 0x0000
    -    self._hk_data["spw_parity_error"] = 0x0000
    -    self._hk_data["spw_link_disconnect"] = 0x0000
    -    self._hk_data["spw_link_running"] = 0x0001
    -
    -    self._hk_data["frame_counter"] = 0x0000
    -    self._hk_data["op_mode"] = 0x0000
    -    self._hk_data["frame_number"] = 0x0000
    -
    -    self._hk_data["error_flags"] = 0x0000
    -
    -    self._hk_data["FPGA minor version"] = 0x0018
    -    self._hk_data["FPGA major version"] = 0x0000
    -    self._hk_data["Board ID"] = 0x0000
    -
    -
    def initialise_hk_header(self)
    @@ -1559,8 +1881,8 @@

    Methods

    def run(self):
     
         LOGGER.info(f"FEE Simulator Current State: {self.decode_current_state()}")
    -    n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum
    -    sensor_sel_enum = GlobalState.setup.camera.fee.sensor_sel.enum
    +    n_fee_side = self.setup.camera.fee.ccd_sides.enum
    +    sensor_sel_enum = self.setup.camera.fee.sensor_sel.enum
     
         with self._transport:
     
    @@ -1578,15 +1900,21 @@ 

    Methods

    sync_period = self.get_sync_period() + if self._killer.term_signal_received: + LOGGER.warning(f"Terminating N-FEE Simulator after receiving {self._killer.signal_name}") + break + if time.perf_counter() <= start_time + sync_period: continue + # LOGGER.debug(f"{'-'*80} {sync_period = }") + start_time = time.perf_counter() # Send a timecode packet ----------------------------------------------------------- timecode = self._hk_data.timecode - LOGGER.info(f"Sending timecode=0x{timecode:02X}") + LOGGER.info(f"Sending timecode: 0x{timecode:02X} ({timecode})") self._transport.send_timecode(timecode) # Send a housekeeping packet ------------------------------------------------------- @@ -1594,7 +1922,7 @@

    Methods

    LOGGER.info(f"Sending HK packet: frame_counter={self._hk_data.frame_counter}, " f"sequence_counter={self._hk_header.sequence_counter}, " f"type={self._hk_header.type_as_object}") - LOGGER.info(f"error_flags={self._hk_data['error_flags']}") + LOGGER.debug(f"HK Packet: error_flags=0b{self._hk_data['error_flags']:0b}") data = self._hk_header.data_as_bytes() + self._hk_data.data_as_bytes() packet = SpaceWirePacket.create_packet(data) @@ -1602,6 +1930,13 @@

    Methods

    self._transport.flush() + # Check if error flags must be cleared --------------------------------------------- + + if self.register_map['clear_error_flag']: + LOGGER.info("Clearing error flags") + self.register_map[('reg_21_config', 'clear_error_flag')] = 0 + self._hk_data['error_flags'] = 0 + # Send the Data packets ------------------------------------------------------------ LOGGER.info(f"mode={self.decode_current_state()}, " @@ -1617,25 +1952,39 @@

    Methods

    n_fee_mode.FULL_IMAGE_MODE,)): try: - ccd_id_to_bin = GlobalState.setup.camera.fee.ccd_numbering.CCD_ID_TO_BIN + ccd_id_to_bin = self.setup.camera.fee.ccd_numbering.CCD_ID_TO_BIN except AttributeError: raise SetupError("No entry in the setup for camera.fee.ccd_numbering.CCD_ID_TO_BIN") + LOGGER.info("Sending Data packets...") + ccd_number = ccd_id_to_bin[self.get_current_ccd_number()] # N-FEE CCD number [0-3] if self.sensor_sel & sensor_sel_enum.E_SIDE: - self.send_data_packets(timecode, ccd_id=ccd_number, ccd_side=n_fee_side.E_SIDE) + send_data_packets( + self._transport, timecode, + ccd_id=ccd_number, ccd_side=n_fee_side.E_SIDE, frame_number=self._hk_data.frame_number, + ccd_mode=self._IN.ccd_mode_config, v_start=self.v_start, v_end=self.v_end, setup=self.setup + ) if self.sensor_sel & sensor_sel_enum.F_SIDE: - self.send_data_packets(timecode, ccd_id=ccd_number, ccd_side=n_fee_side.F_SIDE) + send_data_packets( + self._transport, timecode, + ccd_id=ccd_number, ccd_side=n_fee_side.F_SIDE, frame_number=self._hk_data.frame_number, + ccd_mode=self._IN.ccd_mode_config, v_start=self.v_start, v_end=self.v_end, setup=self.setup + ) + + if self._killer.user_signal_received and self._killer.signal_name == "SIGUSR1": + # On USR1 signal, set the error flags to a random number. There are currently 12 error flags. + self.set_error_flags(random.randint(0x0, 0b1111_1111_1111)) + self._killer.clear() # Process any RMAP commands that were sent ----------------------------------------- - LOGGER.info( - f"Calling process_rmap_commands() method for " - f"{self._transport.__class__.__name__}." - ) + LOGGER.info("Processing RMAP commands...") while time.perf_counter() < start_time + sync_period - 0.2: - process_rmap_command(self._transport, self.register_map) + process_rmap_command(self._transport, self.register_map, self._hk_data) + + LOGGER.info("Updating internals...") self._hk_data.increment_timecode() @@ -1648,53 +1997,22 @@

    Methods

    self.update_hk_header(400) else: self._IN.update(200) - self.update_hk_header(200) - - self.update_error_flags()
    - - -
    -def send_data_packets(self, timecode: int, ccd_id: int, ccd_side: int) -
    -
    -
    -
    - -Expand source code - -
    def send_data_packets(self, timecode: int, ccd_id: int, ccd_side: int):
    -
    -        data = create_pattern_data(timecode, ccd_id, ccd_side)
    -
    -        header = DataPacketHeader()
    -        packet_type = header.type_as_object
    -        packet_type.ccd_side = ccd_side
    -        packet_type.ccd_number = ccd_id
    -        packet_type.last_packet = False
    -        packet_type.frame_number = self._hk_data.frame_number
    -        packet_type.mode = self._IN.ccd_mode_config
    -        header.type = packet_type
    -
    -        LOGGER.info(f"**** {packet_type=!s}")
    -        LOGGER.info(f"Sending data packets...{self.v_start=} {self.v_end=}")
    -
    -        for packet in generate_data_packets(data, header, self.v_start, self.v_end):
    -            self._transport.write_packet(packet.packet_as_bytes)
    -            self._transport.flush()
    + self.update_hk_header(200)
    -
    -def update_error_flags(self) +
    +def set_error_flags(self, flags: int)
    -

    Set error_flags to 0b0000_1000_1000 (136) whenever the frame counter is a multiple of 5.

    +

    Set error_flags to the given number.

    Expand source code -
    def update_error_flags(self):
    -    """Set error_flags to 0b0000_1000_1000 (136) whenever the frame counter is a multiple of 5."""
    -    self._hk_data["error_flags"] = 0b0000_0000_0000 if self._hk_data.frame_counter % 5 else 0b0000_1000_1000
    +
    def set_error_flags(self, flags: int):
    +    """Set error_flags to the given number."""
    +    LOGGER.warning(f"Setting error flags to 0b{flags:012b}")
    +    self._hk_data["error_flags"] = flags
    @@ -1804,9 +2122,9 @@

    Methods

    Returns: The sync period (time between two sync pulses) in seconds. """ - sync_sel = self.reg_map.get_value("reg_5_config", "sync_sel") + sync_sel = self.sync_sel if sync_sel: - sync_period = self.reg_map.get_value("reg_4_config", "int_sync_period") + sync_period = self.int_sync_period + 400 # add the duration of the long pulse else: sync_period = 6250 return sync_period / 1000 @@ -1858,9 +2176,9 @@

    Returns

    Returns: The sync period (time between two sync pulses) in seconds. """ - sync_sel = self.reg_map.get_value("reg_5_config", "sync_sel") + sync_sel = self.sync_sel if sync_sel: - sync_period = self.reg_map.get_value("reg_4_config", "int_sync_period") + sync_period = self.int_sync_period + 400 # add the duration of the long pulse else: sync_period = 6250 return sync_period / 1000
    @@ -2003,10 +2321,14 @@

    Index

  • Functions

  • @@ -2019,13 +2341,11 @@

    get_current_ccd_number
  • get_mode
  • get_sync_period
  • -
  • initialise_hk_data
  • initialise_hk_header
  • internal_sync
  • run
  • -
  • send_data_packets
  • sensor_sel
  • -
  • update_error_flags
  • +
  • set_error_flags
  • update_hk_data
  • update_hk_header
  • v_end
  • diff --git a/docs/api/egse/fee/index.html b/docs/api/egse/fee/index.html index dfbafd4..11e462d 100644 --- a/docs/api/egse/fee/index.html +++ b/docs/api/egse/fee/index.html @@ -39,14 +39,6 @@

    Module egse.fee

    from enum import IntEnum from typing import List -from egse.fee.fee import get_data -from egse.fee.fee import get_mode -from egse.fee.fee import increment_timecode -from egse.fee.fee import is_data_data_packet -from egse.fee.fee import is_hk_data_packet -from egse.fee.fee import is_last_packet -from egse.fee.fee import is_overscan_data_packet - from egse.setup import SetupError from egse.state import GlobalState @@ -149,10 +141,6 @@

    Module egse.fee

    Sub-modules

    -
    egse.fee.fee
    -
    -

    This module contains functions and classes that are typically used by the FEE.

    -
    egse.fee.feesim

    The FEE Simulator application. This app provides basic FEE functionality and is used mainly to @@ -419,7 +407,6 @@

    Index

  • Sub-modules

      -
    • egse.fee.fee
    • egse.fee.feesim
    • egse.fee.n_fee_hk
    • egse.fee.nfee
    • diff --git a/docs/api/egse/fee/n_fee_hk.html b/docs/api/egse/fee/n_fee_hk.html index ae620e7..ef58b79 100644 --- a/docs/api/egse/fee/n_fee_hk.html +++ b/docs/api/egse/fee/n_fee_hk.html @@ -59,8 +59,7 @@

      Module egse.fee.n_fee_hk

      from egse.hk import read_conversion_dict from egse.metrics import define_metrics from egse.settings import Settings -from egse.setup import NavigableDict, SetupError -from egse.state import GlobalState +from egse.setup import NavigableDict, SetupError, load_setup, Setup from egse.storage import StorageProxy from egse.storage import register_to_storage_manager from egse.storage import unregister_from_storage_manager @@ -82,6 +81,8 @@

      Module egse.fee.n_fee_hk

      HK_TIMESTAMP_NAME = "timestamp" +KNOWN_KEYS_NOT_IN_METRICS = ['timestamp', 'timecode_ts'] + class GeneratorSignals(QObject): finished = pyqtSignal() @@ -197,6 +198,9 @@

      Module egse.fee.n_fee_hk

      def __getitem__(self, item): return self._data[item] + def __contains__(self, item): + return item in self._data + def get(self, item): try: return self._data[item] @@ -207,7 +211,7 @@

      Module egse.fee.n_fee_hk

      return self._keys def values(self): - return [self._data[name] for name in self._keys] + return self._data.values() def update(self, data: dict): if x := set(data) - set(self._data): @@ -219,6 +223,9 @@

      Module egse.fee.n_fee_hk

      def clear(self): self._data = {k: None for k in self._keys} + def as_dict(self): + return self._data + class MainWindow(QMainWindow): @@ -227,8 +234,8 @@

      Module egse.fee.n_fee_hk

      self.hk_generator = HousekeepingGenerator() - column_names = self.hk_generator.current_data.keys() - register_to_storage_manager(ORIGIN, CSV, prep=dict(mode='a', column_names=column_names)) + column_names = self.hk_generator.hk_names_mapping.values() + register_to_storage_manager(ORIGIN, CSV, prep=dict(mode='a', column_names=list(column_names))) self.hk_generator.signals.finished.connect(self.close) self.hk_generator.run() @@ -253,6 +260,7 @@

      Module egse.fee.n_fee_hk

      def __init__(self): self.signals = GeneratorSignals() + self.setup = load_setup() # Convert with info from HK info table # Append the new names @@ -264,19 +272,21 @@

      Module egse.fee.n_fee_hk

      # names to the correct device names as defined in the CGSE. The keys in the mapping are # the original device name, the values are the CGSE corrected names - self.hk_names_mapping = read_conversion_dict(ORIGIN, use_site=False) - hk_header = self.hk_names_mapping.values() + self.hk_names_mapping = read_conversion_dict(ORIGIN, use_site=False, setup=self.setup) + + # Add the timestamp and timecode_ts to the names mapping + self.hk_names_mapping.update({'timestamp': 'timestamp', 'timecode_ts': 'timecode_ts'}) + + hk_header = self.hk_names_mapping.keys() # Read from the setup: sensor calibration data (as a NavigableDict) - self.supply_voltage_calibration = GlobalState.setup.camera.fee.calibration.supply_voltages - self.temperature_calibration = GlobalState.setup.camera.fee.calibration.temperatures + self.supply_voltage_calibration = self.setup.camera.fee.calibration.supply_voltages + self.temperature_calibration = self.setup.camera.fee.calibration.temperatures - self.current_data = DataCollector( - [HK_TIMESTAMP_NAME, *hk_header] - ) + self.current_data = DataCollector([HK_TIMESTAMP_NAME, *hk_header]) - self.hk_metrics = define_metrics(ORIGIN, dashboard='*') + self.hk_metrics = define_metrics(ORIGIN, dashboard='*', setup=self.setup) def run(self): self.start_pulling_data() @@ -292,39 +302,49 @@

      Module egse.fee.n_fee_hk

      def update_metrics(self): - for metric_name in self.hk_names_mapping.values(): - # LOGGER.info(f"{metric_name=}") - value = self.current_data.get(metric_name) - if value is not None: - self.hk_metrics[metric_name].set(value) - else: - self.hk_metrics[metric_name].set(float('nan')) - LOGGER.log(logging.DEBUG, f"No current data available for {metric_name}.") + for orig_name, metric_name in self.hk_names_mapping.items(): + # LOGGER.info(f"{orig_name=}, {metric_name=}") + try: + value = self.current_data.get(orig_name) + if value is not None: + self.hk_metrics[metric_name].set(value) + else: + self.hk_metrics[metric_name].set(float('nan')) + # LOGGER.debug(f"No current data available for {metric_name}.") + except KeyError as exc: + if metric_name not in KNOWN_KEYS_NOT_IN_METRICS: + LOGGER.warning(f"Unknown metric name: {orig_name=}, {metric_name=}") def send_hk_data_to_storage(self): - if not any(self.current_data.values()): + # All saved data needs at least a timestamp + + if 'timestamp' not in self.current_data or self.current_data['timestamp'] is None: return # Translate the HK names (i.e. start using the correct prefix) # and append them to the rest of the HK - self.current_data = convert_hk_names(dict(zip(self.current_data.keys(), self.current_data.values())), - self.hk_names_mapping) + # CHANGED: Remember self.hk_names_mapping shall contain both timestamp and timecode_ts. + # Be careful here, because the convert_hk_names() returns a new dict while the + # self.current_data is a DataCollector. So, don't catch the returned dict in the + # self.current_data. + current_data = convert_hk_names(self.current_data.as_dict(), self.hk_names_mapping) + # Calibration (supply voltages + temperatures) - calibrated_supply_voltages = get_calibrated_supply_voltages(self.current_data, - self.supply_voltage_calibration) - self.current_data.update(calibrated_supply_voltages) - calibrated_temperatures = get_calibrated_temperatures(self.current_data, self.temperature_calibration) - self.current_data.update(calibrated_temperatures) + calibrated_supply_voltages = get_calibrated_supply_voltages(current_data, self.supply_voltage_calibration) + current_data.update(calibrated_supply_voltages) + + calibrated_temperatures = get_calibrated_temperatures(current_data, self.temperature_calibration, self.setup) + current_data.update(calibrated_temperatures) with StorageProxy() as storage: rc = storage.save( { "origin": ORIGIN, - "data": self.current_data + "data": current_data } ) if rc and not rc.successful: @@ -335,7 +355,7 @@

      Module egse.fee.n_fee_hk

      if len(calibrated_temperatures) > 0: - calibrated_temperatures["timestamp"] = self.current_data[HK_TIMESTAMP_NAME] + calibrated_temperatures["timestamp"] = current_data[HK_TIMESTAMP_NAME] with SynopticsManagerProxy() as synoptics: synoptics.store_common_synoptics(calibrated_temperatures) @@ -343,37 +363,47 @@

      Module egse.fee.n_fee_hk

      def worker_output(self, sync_id, data): if sync_id == MessageIdentifier.SYNC_TIMECODE: - self.send_hk_data_to_storage() - self.update_metrics() self.current_data.clear() + timecode, timestamp = data - LOGGER.info(f"Timecode: {timecode}") + LOGGER.debug(f"Timecode: {timestamp=}, {timecode=}") self.current_data.update({"timecode": timecode, "timecode_ts": timestamp}) if sync_id == MessageIdentifier.SYNC_HK_PACKET: hk_packet, timestamp = data - LOGGER.info(f"HK Packet: {hk_packet.type=!s}") - - self.current_data.update({HK_TIMESTAMP_NAME: timestamp}) + LOGGER.debug(f"HK Packet: {timestamp=}, {hk_packet.type=!s}") hk_data = HousekeepingData(hk_packet.data) + self._store_hk_data(hk_data, timestamp) + + if sync_id == MessageIdentifier.SYNC_HK_DATA: + hk_data, timestamp = data + LOGGER.debug(f"HK Data: {timestamp=}") - for par_name in hk_data: - self.current_data.update({par_name: hk_data[par_name]}) + self._store_hk_data(hk_data, timestamp) if sync_id == MessageIdentifier.NUM_CYCLES: - LOGGER.info(f"num_cycles: {max(0, data)}") + LOGGER.debug(f"num_cycles: {max(0, data)}") self.current_data.update({'num_cycles': max(0, data)}) + def _store_hk_data(self, hk_data: HousekeepingData, timestamp: str): + self.current_data.update({HK_TIMESTAMP_NAME: timestamp}) + + for par_name in hk_data: + self.current_data.update({par_name: hk_data[par_name]}) + + self.send_hk_data_to_storage() + self.update_metrics() + def worker_complete(self): LOGGER.info("THREAD COMPLETE!") LOGGER.info(f"Number of threads running: {self.threadpool.activeThreadCount()}") self.data_puller = None self.signals.finished.emit() - def worker_error(self, t): - LOGGER.warning("ERROR: %s" % t) + def worker_error(self, msg): + LOGGER.warning(f"ERROR: {msg}") def get_calibrated_supply_voltages(counts, supply_voltage_calibration): @@ -395,7 +425,7 @@

      Module egse.fee.n_fee_hk

      for cal_name in supply_voltage_calibration: - raw_name = cal_name + "_RAW" + raw_name = f"{cal_name}_RAW" cal = supply_voltage_calibration[cal_name] if counts[raw_name] is not None: @@ -407,7 +437,7 @@

      Module egse.fee.n_fee_hk

      return supply_voltages -def get_calibrated_temperatures(counts, sensor_calibration): +def get_calibrated_temperatures(counts, sensor_calibration, setup): """ Calibrate the N-FEE temperatures. The calibrated temperatures come from: @@ -422,6 +452,7 @@

      Module egse.fee.n_fee_hk

      Args: - counts: Uncalibrated, raw data for all HK. - sensor_calibration: N-FEE temperature calibration as read from the YAML file that was specified in the setup + - setup: Setup Returns: Dictionary with calibrated temperatures [°C]. """ @@ -429,18 +460,19 @@

      Module egse.fee.n_fee_hk

      temperatures = {} for sensor_type in sensor_calibration: - temperatures.update(get_calibrated_temperatures_for_type(counts, sensor_calibration, sensor_type)) + temperatures.update(get_calibrated_temperatures_for_type(counts, sensor_calibration, sensor_type, setup)) return temperatures -def get_calibrated_temperatures_for_type(counts, sensor_calibration, sensor_type) -> dict: +def get_calibrated_temperatures_for_type(counts, sensor_calibration, sensor_type, setup) -> dict: """ Calibrate the N-FEE temperatures for the given sensor type. Args: - counts: Uncalibrated, raw data [ADU] - sensor_calibration: Calibration information for the given sensor type - sensor_calibration: N-FEE sensor calibration as read from the YAML file that was specified in the setup + - setup: Setup Returns: Dictionary with calibrated temperatures [°C] for the given sensor type. """ @@ -451,31 +483,37 @@

      Module egse.fee.n_fee_hk

      for cal_name in cal.sensor_names: - raw_name = cal_name + "_RAW" + if cal_name.endswith("_AMB"): + raw_name = f"{cal_name[:-4]}_RAW" + else: + raw_name = f"{cal_name}_RAW" if counts[raw_name] is not None: sensor_info = cal[cal_name] if cal_name in cal else cal try: - temperature = counts_to_temperature(cal_name, counts[raw_name], sensor_info) + temperature = counts_to_temperature(cal_name, counts[raw_name], sensor_info, setup) except AttributeError as exc: - raise SetupError("Not all TOU TRP PT1000 calibration information is available in the setup under " - "setup.camera.fee.calibration.", {exc}) + raise SetupError( + "Not all TOU TRP PT1000 calibration information is available in the setup under " + "setup.camera.fee.calibration." + ) from exc temperatures[cal_name] = temperature return temperatures -def counts_to_temperature(sensor_name: str, counts: float, sensor_info: NavigableDict): +def counts_to_temperature(sensor_name: str, counts: float, sensor_info: NavigableDict, setup): """ Convert the given counts for the given sensor to temperature. Args: - sensor_name: Sensor name - counts: Uncalibrated, raw data [ADU] - sensor_info: Calibration information for the given sensor (type) + - setup: Setup Returns: Calibrated temperature [°C] for the given sensor """ @@ -487,9 +525,8 @@

      Module egse.fee.n_fee_hk

      # Conversion: counts -> resistance -> temperature - else: - resistance = counts_to_resistance(sensor_name, counts, sensor_info) - return resistance_to_temperature(sensor_name, resistance, sensor_info) + resistance = counts_to_resistance(sensor_name, counts, sensor_info) + return resistance_to_temperature(sensor_name, resistance, sensor_info, setup) def counts_to_resistance(sensor_name: str, counts: float, sensor_info: NavigableDict): @@ -524,13 +561,14 @@

      Module egse.fee.n_fee_hk

      raise SetupError(f"Setup does not contain info for conversion from counts to resistance for {sensor_name}") -def resistance_to_temperature(sensor_name: str, resistance: float, sensor_info: NavigableDict): +def resistance_to_temperature(sensor_name: str, resistance: float, sensor_info: NavigableDict, setup): """ Convert the given resistance for the given sensor to counts. Args: - sensor_name: Sensor name - resistance: Resistance [Ohm] - sensor_info: Calibration information for the given sensor (type) + - setup: Setup Returns: Temperature [°C] for the given sensor. """ @@ -548,6 +586,9 @@

      Module egse.fee.n_fee_hk

      method: str = resistance_to_temperature_info.method + if "divide_resistance_by" in resistance_to_temperature_info: + resistance /= resistance_to_temperature_info.divide_resistance_by + # Polynomial if method == "polynomial": @@ -562,15 +603,13 @@

      Module egse.fee.n_fee_hk

      if "resistance_to_temperature_coefficients" in resistance_to_temperature_info: return np.polyval(resistance_to_temperature_info.resistance_to_temperature_coefficients, resistance) - # Callendar - van Dusen equation + if method != "callendar_van_dusen": + raise SetupError(f"Setup does not contain info for conversion from resistance to temperature for {sensor_name}") - if method == "callendar_van_dusen": - standard = resistance_to_temperature_info.standard - ref_resistance = resistance_to_temperature_info.ref_resistance - return callendar_van_dusen(resistance, ref_resistance, standard) + standard = resistance_to_temperature_info.standard + ref_resistance = resistance_to_temperature_info.ref_resistance - else: - raise SetupError(f"Setup does not contain info for conversion from resistance to temperature for {sensor_name}") + return callendar_van_dusen(resistance, ref_resistance, standard, setup) def solve_temperature(temperature_to_resistance_coefficients, resistance): @@ -592,19 +631,21 @@

      Module egse.fee.n_fee_hk

      return temperature.real -def callendar_van_dusen(resistance, ref_resistance, standard): +def callendar_van_dusen(resistance, ref_resistance, standard, setup): """ Solve the Callendar - van Dusen equation for temperature. Args: - resistance: Resistance [Ohm] for which to calculate the temperature - ref_resistance: Resistance [Ohm] for a temperature of 0°C + - standard: Sensor standard + - setup: Setup. Return: Temperature [°C] corresponding to the given resistance. """ # Resistances higher than the reference resistance correspond to - coefficients = GlobalState.setup.sensor_calibration.callendar_van_dusen[standard] + coefficients = setup.sensor_calibration.callendar_van_dusen[standard] # Positive temperatures @@ -667,7 +708,7 @@

      Module egse.fee.n_fee_hk

      def sigint_handler(*args): """Handler for the SIGINT signal.""" - LOGGER.info(f'Handling signal SIGINT...') + LOGGER.info('Handling signal SIGINT...') if QApplication.platformName() == "offscreen": window.close() QApplication.quit() @@ -763,33 +804,37 @@

      Module egse.fee.n_fee_hk

      Functions

      -def callendar_van_dusen(resistance, ref_resistance, standard) +def callendar_van_dusen(resistance, ref_resistance, standard, setup)

      Solve the Callendar - van Dusen equation for temperature.

      Args

      • resistance: Resistance [Ohm] for which to calculate the temperature
      • -
      • ref_resistance: Resistance [Ohm] for a temperature of 0°C +
      • ref_resistance: Resistance [Ohm] for a temperature of 0°C
      • +
      • standard: Sensor standard
      • +
      • setup: Setup. Return: Temperature [°C] corresponding to the given resistance.
      Expand source code -
      def callendar_van_dusen(resistance, ref_resistance, standard):
      +
      def callendar_van_dusen(resistance, ref_resistance, standard, setup):
           """ Solve the Callendar - van Dusen equation for temperature.
       
           Args:
               - resistance: Resistance [Ohm] for which to calculate the temperature
               - ref_resistance: Resistance [Ohm] for a temperature of 0°C
      +        - standard: Sensor standard
      +        - setup: Setup.
       
           Return: Temperature [°C] corresponding to the given resistance.
           """
       
           # Resistances higher than the reference resistance correspond to
       
      -    coefficients = GlobalState.setup.sensor_calibration.callendar_van_dusen[standard]
      +    coefficients = setup.sensor_calibration.callendar_van_dusen[standard]
       
           # Positive temperatures
       
      @@ -858,7 +903,7 @@ 

      Args

      -def counts_to_temperature(sensor_name: str, counts: float, sensor_info: egse.setup.NavigableDict) +def counts_to_temperature(sensor_name: str, counts: float, sensor_info: egse.setup.NavigableDict, setup)

      Convert the given counts for the given sensor to temperature.

      @@ -866,20 +911,22 @@

      Args

      • sensor_name: Sensor name
      • counts: Uncalibrated, raw data [ADU]
      • -
      • sensor_info: Calibration information for the given sensor (type) +
      • sensor_info: Calibration information for the given sensor (type)
      • +
      • setup: Setup Returns: Calibrated temperature [°C] for the given sensor
      Expand source code -
      def counts_to_temperature(sensor_name: str, counts: float, sensor_info: NavigableDict):
      +
      def counts_to_temperature(sensor_name: str, counts: float, sensor_info: NavigableDict, setup):
           """ Convert the given counts for the given sensor to temperature.
       
           Args:
               - sensor_name: Sensor name
               - counts: Uncalibrated, raw data [ADU]
               - sensor_info: Calibration information for the given sensor (type)
      +        - setup: Setup
       
           Returns: Calibrated temperature [°C] for the given sensor
           """
      @@ -891,9 +938,8 @@ 

      Args

      # Conversion: counts -> resistance -> temperature - else: - resistance = counts_to_resistance(sensor_name, counts, sensor_info) - return resistance_to_temperature(sensor_name, resistance, sensor_info)
      + resistance = counts_to_resistance(sensor_name, counts, sensor_info) + return resistance_to_temperature(sensor_name, resistance, sensor_info, setup)
      @@ -933,7 +979,7 @@

      Args

      for cal_name in supply_voltage_calibration: - raw_name = cal_name + "_RAW" + raw_name = f"{cal_name}_RAW" cal = supply_voltage_calibration[cal_name] if counts[raw_name] is not None: @@ -946,7 +992,7 @@

      Args

  • -def get_calibrated_temperatures(counts, sensor_calibration) +def get_calibrated_temperatures(counts, sensor_calibration, setup)

    Calibrate the N-FEE temperatures.

    @@ -960,14 +1006,15 @@

    Args

    Args

    • counts: Uncalibrated, raw data for all HK.
    • -
    • sensor_calibration: N-FEE temperature calibration as read from the YAML file that was specified in the setup +
    • sensor_calibration: N-FEE temperature calibration as read from the YAML file that was specified in the setup
    • +
    • setup: Setup Returns: Dictionary with calibrated temperatures [°C].
    Expand source code -
    def get_calibrated_temperatures(counts, sensor_calibration):
    +
    def get_calibrated_temperatures(counts, sensor_calibration, setup):
         """ Calibrate the N-FEE temperatures.
     
         The calibrated temperatures come from:
    @@ -982,6 +1029,7 @@ 

    Args

    Args: - counts: Uncalibrated, raw data for all HK. - sensor_calibration: N-FEE temperature calibration as read from the YAML file that was specified in the setup + - setup: Setup Returns: Dictionary with calibrated temperatures [°C]. """ @@ -989,13 +1037,13 @@

    Args

    temperatures = {} for sensor_type in sensor_calibration: - temperatures.update(get_calibrated_temperatures_for_type(counts, sensor_calibration, sensor_type)) + temperatures.update(get_calibrated_temperatures_for_type(counts, sensor_calibration, sensor_type, setup)) return temperatures
    -def get_calibrated_temperatures_for_type(counts, sensor_calibration, sensor_type) ‑> dict +def get_calibrated_temperatures_for_type(counts, sensor_calibration, sensor_type, setup) ‑> dict

    Calibrate the N-FEE temperatures for the given sensor type.

    @@ -1003,20 +1051,22 @@

    Args

    • counts: Uncalibrated, raw data [ADU]
    • sensor_calibration: Calibration information for the given sensor type
    • -
    • sensor_calibration: N-FEE sensor calibration as read from the YAML file that was specified in the setup +
    • sensor_calibration: N-FEE sensor calibration as read from the YAML file that was specified in the setup
    • +
    • setup: Setup Returns: Dictionary with calibrated temperatures [°C] for the given sensor type.
    Expand source code -
    def get_calibrated_temperatures_for_type(counts, sensor_calibration, sensor_type) -> dict:
    +
    def get_calibrated_temperatures_for_type(counts, sensor_calibration, sensor_type, setup) -> dict:
         """ Calibrate the N-FEE temperatures for the given sensor type.
     
         Args:
             - counts: Uncalibrated, raw data [ADU]
             - sensor_calibration: Calibration information for the given sensor type
             - sensor_calibration: N-FEE sensor calibration as read from the YAML file that was specified in the setup
    +        - setup: Setup
     
         Returns: Dictionary with calibrated temperatures [°C] for the given sensor type.
         """
    @@ -1027,18 +1077,23 @@ 

    Args

    for cal_name in cal.sensor_names: - raw_name = cal_name + "_RAW" + if cal_name.endswith("_AMB"): + raw_name = f"{cal_name[:-4]}_RAW" + else: + raw_name = f"{cal_name}_RAW" if counts[raw_name] is not None: sensor_info = cal[cal_name] if cal_name in cal else cal try: - temperature = counts_to_temperature(cal_name, counts[raw_name], sensor_info) + temperature = counts_to_temperature(cal_name, counts[raw_name], sensor_info, setup) except AttributeError as exc: - raise SetupError("Not all TOU TRP PT1000 calibration information is available in the setup under " - "setup.camera.fee.calibration.", {exc}) + raise SetupError( + "Not all TOU TRP PT1000 calibration information is available in the setup under " + "setup.camera.fee.calibration." + ) from exc temperatures[cal_name] = temperature @@ -1046,7 +1101,7 @@

    Args

    -def resistance_to_temperature(sensor_name: str, resistance: float, sensor_info: egse.setup.NavigableDict) +def resistance_to_temperature(sensor_name: str, resistance: float, sensor_info: egse.setup.NavigableDict, setup)

    Convert the given resistance for the given sensor to counts.

    @@ -1054,20 +1109,22 @@

    Args

    • sensor_name: Sensor name
    • resistance: Resistance [Ohm]
    • -
    • sensor_info: Calibration information for the given sensor (type) +
    • sensor_info: Calibration information for the given sensor (type)
    • +
    • setup: Setup Returns: Temperature [°C] for the given sensor.
    Expand source code -
    def resistance_to_temperature(sensor_name: str, resistance: float, sensor_info: NavigableDict):
    +
    def resistance_to_temperature(sensor_name: str, resistance: float, sensor_info: NavigableDict, setup):
         """ Convert the given resistance for the given sensor to counts.
     
         Args:
             - sensor_name: Sensor name
             - resistance: Resistance [Ohm]
             - sensor_info: Calibration information for the given sensor (type)
    +        - setup: Setup
     
         Returns: Temperature [°C] for the given sensor.
         """
    @@ -1085,6 +1142,9 @@ 

    Args

    method: str = resistance_to_temperature_info.method + if "divide_resistance_by" in resistance_to_temperature_info: + resistance /= resistance_to_temperature_info.divide_resistance_by + # Polynomial if method == "polynomial": @@ -1099,15 +1159,13 @@

    Args

    if "resistance_to_temperature_coefficients" in resistance_to_temperature_info: return np.polyval(resistance_to_temperature_info.resistance_to_temperature_coefficients, resistance) - # Callendar - van Dusen equation + if method != "callendar_van_dusen": + raise SetupError(f"Setup does not contain info for conversion from resistance to temperature for {sensor_name}") - if method == "callendar_van_dusen": - standard = resistance_to_temperature_info.standard - ref_resistance = resistance_to_temperature_info.ref_resistance - return callendar_van_dusen(resistance, ref_resistance, standard) + standard = resistance_to_temperature_info.standard + ref_resistance = resistance_to_temperature_info.ref_resistance - else: - raise SetupError(f"Setup does not contain info for conversion from resistance to temperature for {sensor_name}")
    + return callendar_van_dusen(resistance, ref_resistance, standard, setup)
    @@ -1200,6 +1258,9 @@

    Classes

    def __getitem__(self, item): return self._data[item] + def __contains__(self, item): + return item in self._data + def get(self, item): try: return self._data[item] @@ -1210,7 +1271,7 @@

    Classes

    return self._keys def values(self): - return [self._data[name] for name in self._keys] + return self._data.values() def update(self, data: dict): if x := set(data) - set(self._data): @@ -1220,10 +1281,26 @@

    Classes

    self._data.update(data) def clear(self): - self._data = {k: None for k in self._keys}
    + self._data = {k: None for k in self._keys} + + def as_dict(self): + return self._data

    Methods

    +
    +def as_dict(self) +
    +
    +
    +
    + +Expand source code + +
    def as_dict(self):
    +    return self._data
    +
    +
    def clear(self)
    @@ -1293,7 +1370,7 @@

    Methods

    Expand source code
    def values(self):
    -    return [self._data[name] for name in self._keys]
    + return self._data.values()
    @@ -1304,7 +1381,7 @@

    Methods

    QRunnable() -QRunnable(QRunnable)

    +QRunnable(a0: QRunnable)

    Expand source code @@ -1482,7 +1559,7 @@

    Methods

    (*args, **kwargs)
    -

    QObject(parent: QObject = None)

    +

    QObject(parent: typing.Optional[QObject] = None)

    Expand source code @@ -1520,6 +1597,7 @@

    Methods

    def __init__(self): self.signals = GeneratorSignals() + self.setup = load_setup() # Convert with info from HK info table # Append the new names @@ -1531,19 +1609,21 @@

    Methods

    # names to the correct device names as defined in the CGSE. The keys in the mapping are # the original device name, the values are the CGSE corrected names - self.hk_names_mapping = read_conversion_dict(ORIGIN, use_site=False) - hk_header = self.hk_names_mapping.values() + self.hk_names_mapping = read_conversion_dict(ORIGIN, use_site=False, setup=self.setup) + + # Add the timestamp and timecode_ts to the names mapping + self.hk_names_mapping.update({'timestamp': 'timestamp', 'timecode_ts': 'timecode_ts'}) + + hk_header = self.hk_names_mapping.keys() # Read from the setup: sensor calibration data (as a NavigableDict) - self.supply_voltage_calibration = GlobalState.setup.camera.fee.calibration.supply_voltages - self.temperature_calibration = GlobalState.setup.camera.fee.calibration.temperatures + self.supply_voltage_calibration = self.setup.camera.fee.calibration.supply_voltages + self.temperature_calibration = self.setup.camera.fee.calibration.temperatures - self.current_data = DataCollector( - [HK_TIMESTAMP_NAME, *hk_header] - ) + self.current_data = DataCollector([HK_TIMESTAMP_NAME, *hk_header]) - self.hk_metrics = define_metrics(ORIGIN, dashboard='*') + self.hk_metrics = define_metrics(ORIGIN, dashboard='*', setup=self.setup) def run(self): self.start_pulling_data() @@ -1559,39 +1639,49 @@

    Methods

    def update_metrics(self): - for metric_name in self.hk_names_mapping.values(): - # LOGGER.info(f"{metric_name=}") - value = self.current_data.get(metric_name) - if value is not None: - self.hk_metrics[metric_name].set(value) - else: - self.hk_metrics[metric_name].set(float('nan')) - LOGGER.log(logging.DEBUG, f"No current data available for {metric_name}.") + for orig_name, metric_name in self.hk_names_mapping.items(): + # LOGGER.info(f"{orig_name=}, {metric_name=}") + try: + value = self.current_data.get(orig_name) + if value is not None: + self.hk_metrics[metric_name].set(value) + else: + self.hk_metrics[metric_name].set(float('nan')) + # LOGGER.debug(f"No current data available for {metric_name}.") + except KeyError as exc: + if metric_name not in KNOWN_KEYS_NOT_IN_METRICS: + LOGGER.warning(f"Unknown metric name: {orig_name=}, {metric_name=}") def send_hk_data_to_storage(self): - if not any(self.current_data.values()): + # All saved data needs at least a timestamp + + if 'timestamp' not in self.current_data or self.current_data['timestamp'] is None: return # Translate the HK names (i.e. start using the correct prefix) # and append them to the rest of the HK - self.current_data = convert_hk_names(dict(zip(self.current_data.keys(), self.current_data.values())), - self.hk_names_mapping) + # CHANGED: Remember self.hk_names_mapping shall contain both timestamp and timecode_ts. + # Be careful here, because the convert_hk_names() returns a new dict while the + # self.current_data is a DataCollector. So, don't catch the returned dict in the + # self.current_data. + current_data = convert_hk_names(self.current_data.as_dict(), self.hk_names_mapping) + # Calibration (supply voltages + temperatures) - calibrated_supply_voltages = get_calibrated_supply_voltages(self.current_data, - self.supply_voltage_calibration) - self.current_data.update(calibrated_supply_voltages) - calibrated_temperatures = get_calibrated_temperatures(self.current_data, self.temperature_calibration) - self.current_data.update(calibrated_temperatures) + calibrated_supply_voltages = get_calibrated_supply_voltages(current_data, self.supply_voltage_calibration) + current_data.update(calibrated_supply_voltages) + + calibrated_temperatures = get_calibrated_temperatures(current_data, self.temperature_calibration, self.setup) + current_data.update(calibrated_temperatures) with StorageProxy() as storage: rc = storage.save( { "origin": ORIGIN, - "data": self.current_data + "data": current_data } ) if rc and not rc.successful: @@ -1602,7 +1692,7 @@

    Methods

    if len(calibrated_temperatures) > 0: - calibrated_temperatures["timestamp"] = self.current_data[HK_TIMESTAMP_NAME] + calibrated_temperatures["timestamp"] = current_data[HK_TIMESTAMP_NAME] with SynopticsManagerProxy() as synoptics: synoptics.store_common_synoptics(calibrated_temperatures) @@ -1610,37 +1700,47 @@

    Methods

    def worker_output(self, sync_id, data): if sync_id == MessageIdentifier.SYNC_TIMECODE: - self.send_hk_data_to_storage() - self.update_metrics() self.current_data.clear() + timecode, timestamp = data - LOGGER.info(f"Timecode: {timecode}") + LOGGER.debug(f"Timecode: {timestamp=}, {timecode=}") self.current_data.update({"timecode": timecode, "timecode_ts": timestamp}) if sync_id == MessageIdentifier.SYNC_HK_PACKET: hk_packet, timestamp = data - LOGGER.info(f"HK Packet: {hk_packet.type=!s}") - - self.current_data.update({HK_TIMESTAMP_NAME: timestamp}) + LOGGER.debug(f"HK Packet: {timestamp=}, {hk_packet.type=!s}") hk_data = HousekeepingData(hk_packet.data) + self._store_hk_data(hk_data, timestamp) - for par_name in hk_data: - self.current_data.update({par_name: hk_data[par_name]}) + if sync_id == MessageIdentifier.SYNC_HK_DATA: + hk_data, timestamp = data + LOGGER.debug(f"HK Data: {timestamp=}") + + self._store_hk_data(hk_data, timestamp) if sync_id == MessageIdentifier.NUM_CYCLES: - LOGGER.info(f"num_cycles: {max(0, data)}") + LOGGER.debug(f"num_cycles: {max(0, data)}") self.current_data.update({'num_cycles': max(0, data)}) + def _store_hk_data(self, hk_data: HousekeepingData, timestamp: str): + self.current_data.update({HK_TIMESTAMP_NAME: timestamp}) + + for par_name in hk_data: + self.current_data.update({par_name: hk_data[par_name]}) + + self.send_hk_data_to_storage() + self.update_metrics() + def worker_complete(self): LOGGER.info("THREAD COMPLETE!") LOGGER.info(f"Number of threads running: {self.threadpool.activeThreadCount()}") self.data_puller = None self.signals.finished.emit() - def worker_error(self, t): - LOGGER.warning("ERROR: %s" % t) + def worker_error(self, msg): + LOGGER.warning(f"ERROR: {msg}")

    Methods

    @@ -1668,28 +1768,34 @@

    Methods

    def send_hk_data_to_storage(self):
     
    -    if not any(self.current_data.values()):
    +    # All saved data needs at least a timestamp
    +
    +    if 'timestamp' not in self.current_data or self.current_data['timestamp'] is None:
             return
     
         # Translate the HK names (i.e. start using the correct prefix)
         # and append them to the rest of the HK
     
    -    self.current_data = convert_hk_names(dict(zip(self.current_data.keys(), self.current_data.values())),
    -                                         self.hk_names_mapping)
    +    # CHANGED: Remember self.hk_names_mapping shall contain both timestamp and timecode_ts.
    +    #          Be careful here, because the convert_hk_names() returns a new dict while the
    +    #          self.current_data is a DataCollector. So, don't catch the returned dict in the
    +    #          self.current_data.
    +    current_data = convert_hk_names(self.current_data.as_dict(), self.hk_names_mapping)
    +
         # Calibration (supply voltages + temperatures)
     
    -    calibrated_supply_voltages = get_calibrated_supply_voltages(self.current_data,
    -                                                                self.supply_voltage_calibration)
    -    self.current_data.update(calibrated_supply_voltages)
    -    calibrated_temperatures = get_calibrated_temperatures(self.current_data, self.temperature_calibration)
    -    self.current_data.update(calibrated_temperatures)
    +    calibrated_supply_voltages = get_calibrated_supply_voltages(current_data, self.supply_voltage_calibration)
    +    current_data.update(calibrated_supply_voltages)
    +
    +    calibrated_temperatures = get_calibrated_temperatures(current_data, self.temperature_calibration, self.setup)
    +    current_data.update(calibrated_temperatures)
     
         with StorageProxy() as storage:
     
             rc = storage.save(
                 {
                     "origin": ORIGIN,
    -                "data": self.current_data
    +                "data": current_data
                 }
             )
         if rc and not rc.successful:
    @@ -1700,7 +1806,7 @@ 

    Methods

    if len(calibrated_temperatures) > 0: - calibrated_temperatures["timestamp"] = self.current_data[HK_TIMESTAMP_NAME] + calibrated_temperatures["timestamp"] = current_data[HK_TIMESTAMP_NAME] with SynopticsManagerProxy() as synoptics: synoptics.store_common_synoptics(calibrated_temperatures)
    @@ -1736,14 +1842,18 @@

    Methods

    def update_metrics(self):
     
    -    for metric_name in self.hk_names_mapping.values():
    -        # LOGGER.info(f"{metric_name=}")
    -        value = self.current_data.get(metric_name)
    -        if value is not None:
    -            self.hk_metrics[metric_name].set(value)
    -        else:
    -            self.hk_metrics[metric_name].set(float('nan'))
    -            LOGGER.log(logging.DEBUG, f"No current data available for {metric_name}.")
    + for orig_name, metric_name in self.hk_names_mapping.items(): + # LOGGER.info(f"{orig_name=}, {metric_name=}") + try: + value = self.current_data.get(orig_name) + if value is not None: + self.hk_metrics[metric_name].set(value) + else: + self.hk_metrics[metric_name].set(float('nan')) + # LOGGER.debug(f"No current data available for {metric_name}.") + except KeyError as exc: + if metric_name not in KNOWN_KEYS_NOT_IN_METRICS: + LOGGER.warning(f"Unknown metric name: {orig_name=}, {metric_name=}")
    @@ -1763,7 +1873,7 @@

    Methods

    -def worker_error(self, t) +def worker_error(self, msg)
    @@ -1771,8 +1881,8 @@

    Methods

    Expand source code -
    def worker_error(self, t):
    -    LOGGER.warning("ERROR: %s" % t)
    +
    def worker_error(self, msg):
    +    LOGGER.warning(f"ERROR: {msg}")
    @@ -1787,27 +1897,28 @@

    Methods

    def worker_output(self, sync_id, data):
     
         if sync_id == MessageIdentifier.SYNC_TIMECODE:
    -        self.send_hk_data_to_storage()
    -        self.update_metrics()
             self.current_data.clear()
    +
             timecode, timestamp = data
    -        LOGGER.info(f"Timecode: {timecode}")
    +        LOGGER.debug(f"Timecode: {timestamp=}, {timecode=}")
     
             self.current_data.update({"timecode": timecode, "timecode_ts": timestamp})
     
         if sync_id == MessageIdentifier.SYNC_HK_PACKET:
             hk_packet, timestamp = data
    -        LOGGER.info(f"HK Packet: {hk_packet.type=!s}")
    -
    -        self.current_data.update({HK_TIMESTAMP_NAME: timestamp})
    +        LOGGER.debug(f"HK Packet: {timestamp=}, {hk_packet.type=!s}")
     
             hk_data = HousekeepingData(hk_packet.data)
    +        self._store_hk_data(hk_data, timestamp)
     
    -        for par_name in hk_data:
    -            self.current_data.update({par_name: hk_data[par_name]})
    +    if sync_id == MessageIdentifier.SYNC_HK_DATA:
    +        hk_data, timestamp = data
    +        LOGGER.debug(f"HK Data: {timestamp=}")
    +
    +        self._store_hk_data(hk_data, timestamp)
     
         if sync_id == MessageIdentifier.NUM_CYCLES:
    -        LOGGER.info(f"num_cycles: {max(0, data)}")
    +        LOGGER.debug(f"num_cycles: {max(0, data)}")
             self.current_data.update({'num_cycles': max(0, data)})
    @@ -1817,7 +1928,7 @@

    Methods

    class MainWindow
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -1829,8 +1940,8 @@

    Methods

    self.hk_generator = HousekeepingGenerator() - column_names = self.hk_generator.current_data.keys() - register_to_storage_manager(ORIGIN, CSV, prep=dict(mode='a', column_names=column_names)) + column_names = self.hk_generator.hk_names_mapping.values() + register_to_storage_manager(ORIGIN, CSV, prep=dict(mode='a', column_names=list(column_names))) self.hk_generator.signals.finished.connect(self.close) self.hk_generator.run() @@ -1864,7 +1975,7 @@

    Methods

    def closeEvent(self, event: PyQt5.QtGui.QCloseEvent) ‑> None
    -

    closeEvent(self, QCloseEvent)

    +

    closeEvent(self, a0: typing.Optional[QCloseEvent])

    Expand source code @@ -1981,7 +2092,8 @@

    Index

    • DataCollector

      -
        +
          +
        • as_dict
        • clear
        • get
        • keys
        • diff --git a/docs/api/egse/fee/nfee.html b/docs/api/egse/fee/nfee.html index 943789c..4ed8b64 100644 --- a/docs/api/egse/fee/nfee.html +++ b/docs/api/egse/fee/nfee.html @@ -36,13 +36,14 @@

          Module egse.fee.nfee

          from pathlib import Path from typing import Tuple +import numpy as np import rich +from rich.table import Table from egse.bits import clear_bit from egse.bits import set_bit from egse.bits import toggle_bit -from egse.state import GlobalState -from egse.system import AttributeDict +from egse.setup import load_setup THIS_FILE_LOCATION = Path(__file__).parent LOGGER = logging.getLogger(__name__) @@ -63,7 +64,8 @@

          Module egse.fee.nfee

          values. """ - reg_map = GlobalState.setup.camera.fee.hk_map + setup = load_setup() + reg_map = setup.camera.fee.hk_map hk_map = {} start_address = reg_map["start_address"] @@ -101,7 +103,7 @@

          Module egse.fee.nfee

          # TODO: add a sanity check for the len(data) - self._data = bytearray(data or bytes(self.hk_data_length)) + self._data = bytearray(data if data is not None else bytes(self.hk_data_length)) def __getitem__(self, item): hk_def = self.hk_definition[item] @@ -148,6 +150,10 @@

          Module egse.fee.nfee

          def data_as_bytes(self) -> bytes: return bytes(self._data) + @property + def data_as_ndarray(self): + return np.frombuffer(self._data, dtype=np.uint8) + def data_length(self): return len(self._data) @@ -159,6 +165,10 @@

          Module egse.fee.nfee

          def frame_number(self): return self["frame_number"] + @property + def error_flags(self): + return self["error_flags"] + def increment_frame_number(self): frame_number = self["frame_number"] frame_number = 0 if frame_number == 3 else frame_number + 1 @@ -183,7 +193,11 @@

          Module egse.fee.nfee

          def __rich__(self): d = {hk_name: self[hk_name] for hk_name in self.hk_definition} - return AttributeDict(d).__rich__() + table = Table("Parameter", "Value [int]", "Value [hex]", "Value [bin]", title="Housekeeping Data") + for name in self.hk_definition: + value = self[name] + table.add_row(name, str(value), f"0x{value:0x}", f"0b{value:b}") + return table def __iter__(self): return iter(self.hk_definition) @@ -191,10 +205,16 @@

          Module egse.fee.nfee

          if __name__ == "__main__": + import os + from egse.fee.feesim import initialise_hk_data + + os.environ["PLATO_CONF_DATA_LOCATION"] = "/Users/rik/git/plato-cgse-conf/data/CSL1/conf" + hk_data = HousekeepingData() + initialise_hk_data(hk_data) + initialise_hk_data(hk_data) - rich.print(hk_data) - rich.print([k for k in hk_data]) + rich.print(hk_data)
    @@ -237,7 +257,8 @@

    Returns

    values. """ - reg_map = GlobalState.setup.camera.fee.hk_map + setup = load_setup() + reg_map = setup.camera.fee.hk_map hk_map = {} start_address = reg_map["start_address"] @@ -290,7 +311,7 @@

    Classes

    # TODO: add a sanity check for the len(data) - self._data = bytearray(data or bytes(self.hk_data_length)) + self._data = bytearray(data if data is not None else bytes(self.hk_data_length)) def __getitem__(self, item): hk_def = self.hk_definition[item] @@ -337,6 +358,10 @@

    Classes

    def data_as_bytes(self) -> bytes: return bytes(self._data) + @property + def data_as_ndarray(self): + return np.frombuffer(self._data, dtype=np.uint8) + def data_length(self): return len(self._data) @@ -348,6 +373,10 @@

    Classes

    def frame_number(self): return self["frame_number"] + @property + def error_flags(self): + return self["error_flags"] + def increment_frame_number(self): frame_number = self["frame_number"] frame_number = 0 if frame_number == 3 else frame_number + 1 @@ -372,13 +401,41 @@

    Classes

    def __rich__(self): d = {hk_name: self[hk_name] for hk_name in self.hk_definition} - return AttributeDict(d).__rich__() + table = Table("Parameter", "Value [int]", "Value [hex]", "Value [bin]", title="Housekeeping Data") + for name in self.hk_definition: + value = self[name] + table.add_row(name, str(value), f"0x{value:0x}", f"0b{value:b}") + return table def __iter__(self): return iter(self.hk_definition)

    Instance variables

    +
    var data_as_ndarray
    +
    +
    +
    + +Expand source code + +
    @property
    +def data_as_ndarray(self):
    +    return np.frombuffer(self._data, dtype=np.uint8)
    +
    +
    +
    var error_flags
    +
    +
    +
    + +Expand source code + +
    @property
    +def error_flags(self):
    +    return self["error_flags"]
    +
    +
    var frame_counter
    @@ -519,7 +576,9 @@

    Index

    HousekeepingData

    • data_as_bytes
    • +
    • data_as_ndarray
    • data_length
    • +
    • error_flags
    • frame_counter
    • frame_number
    • increment_frame_counter
    • diff --git a/docs/api/egse/filterwheel/eksma/fw8smc4.html b/docs/api/egse/filterwheel/eksma/fw8smc4.html index 417fdc3..d56715d 100644 --- a/docs/api/egse/filterwheel/eksma/fw8smc4.html +++ b/docs/api/egse/filterwheel/eksma/fw8smc4.html @@ -41,17 +41,12 @@

      Module egse.filterwheel.eksma.fw8smc4

      import numpy as np -from egse.confman import ConfigurationManagerProxy from egse.decorators import dynamic_interface from egse.device import DeviceInterface from egse.filterwheel import FilterWheelError -from egse.filterwheel.eksma.fw8smc4_devif import FilterWheel8SMC4Error -from egse.filterwheel.eksma.fw8smc4_devif import FilterWheel8SMC4EthernetInterface -from egse.filterwheel.eksma.fw8smc4_devif import FilterWheel8SMC4USBInterface - - from egse.proxy import Proxy from egse.settings import Settings +from egse.setup import load_setup from egse.zmq_ser import connect_address LOGGER = logging.getLogger(__name__) @@ -79,7 +74,6 @@

      Module egse.filterwheel.eksma.fw8smc4

      """ return NotImplemented - @dynamic_interface def set_position(self): """ @@ -187,22 +181,22 @@

      Module egse.filterwheel.eksma.fw8smc4

      # when the wheel factor is a character, the filter is a spectral filter # will be late replaced by the setup configuration self.wheel1 = {0: 0, - 1: 2, - 2: 1, - 3: 0.03, - 4: "T4K", - 5: "T5K", - 6: "T6K", - 7: "T6.5K"} + 1: 2, + 2: 1, + 3: 0.03, + 4: "T4K", + 5: "T5K", + 6: "T6K", + 7: "T6.5K"} self.wheel2 = {0: 0, - 1: 0.3, - 2: 0.05, - 3: 5, - 4: 2, - 5: 0.1, - 6: 0.15, - 7: 3} + 1: 0.3, + 2: 0.05, + 3: 5, + 4: 2, + 5: 0.1, + 6: 0.15, + 7: 3} self._setup = None self._att_level = [self.wheel1, self.wheel2, 0] @@ -236,7 +230,6 @@

      Module egse.filterwheel.eksma.fw8smc4

      "Current Motor 2": 0, "Current Motor 3": 0} - def is_connected(self): return True @@ -253,8 +246,7 @@

      Module egse.filterwheel.eksma.fw8smc4

      self.connect() def load_wheels(self): - with ConfigurationManagerProxy() as cm: - self._setup = cm.get_setup() + self._setup = load_setup() #todo: still some work here to manage the default setup to be loaded return self._setup @@ -395,18 +387,24 @@

      Module egse.filterwheel.eksma.fw8smc4

      super().__init__() + from egse.filterwheel.eksma.fw8smc4_devif import FilterWheel8SMC4Error + from egse.filterwheel.eksma.fw8smc4_devif import FilterWheel8SMC4EthernetInterface + from egse.filterwheel.eksma.fw8smc4_devif import FilterWheel8SMC4USBInterface + + self._setup = None + LOGGER.debug("Initializing Filter Wheel 8SMC4 Controller") # when the wheel factor is 0 there is no filter in the wheel, if OD units are considered # when the wheel factor is a character, the filter is a spectral filter # will be late replaced by the setup configuration self.wheel1 = {0: 0, - 1: 2, - 2: 1, - 3: 0.03, - 4: "T4K", - 5: "T5K", - 6: "T6K", - 7: "T6.5K"} + 1: 2, + 2: 1, + 3: 0.03, + 4: "T4K", + 5: "T5K", + 6: "T6K", + 7: "T6.5K"} self.wheel2 = {0: 0, 1: 0.3, @@ -490,15 +488,13 @@

      Module egse.filterwheel.eksma.fw8smc4

      return False def load_wheels(self): - with ConfigurationManagerProxy() as cm: - self._setup = cm.get_setup() - #todo: still some work here to manage the default setup to be loaded + self._setup = load_setup() + # todo: still some work here to manage the default setup to be loaded return self._setup def get_id(self): return self.fw.get_id() - def get_response(self, cmd_string): response = self.fw.get_response(cmd_string) return response @@ -721,18 +717,24 @@

      Classes

      super().__init__() + from egse.filterwheel.eksma.fw8smc4_devif import FilterWheel8SMC4Error + from egse.filterwheel.eksma.fw8smc4_devif import FilterWheel8SMC4EthernetInterface + from egse.filterwheel.eksma.fw8smc4_devif import FilterWheel8SMC4USBInterface + + self._setup = None + LOGGER.debug("Initializing Filter Wheel 8SMC4 Controller") # when the wheel factor is 0 there is no filter in the wheel, if OD units are considered # when the wheel factor is a character, the filter is a spectral filter # will be late replaced by the setup configuration self.wheel1 = {0: 0, - 1: 2, - 2: 1, - 3: 0.03, - 4: "T4K", - 5: "T5K", - 6: "T6K", - 7: "T6.5K"} + 1: 2, + 2: 1, + 3: 0.03, + 4: "T4K", + 5: "T5K", + 6: "T6K", + 7: "T6.5K"} self.wheel2 = {0: 0, 1: 0.3, @@ -816,15 +818,13 @@

      Classes

      return False def load_wheels(self): - with ConfigurationManagerProxy() as cm: - self._setup = cm.get_setup() - #todo: still some work here to manage the default setup to be loaded + self._setup = load_setup() + # todo: still some work here to manage the default setup to be loaded return self._setup def get_id(self): return self.fw.get_id() - def get_response(self, cmd_string): response = self.fw.get_response(cmd_string) return response @@ -1203,7 +1203,6 @@

      Inherited members

      """ return NotImplemented - @dynamic_interface def set_position(self): """ @@ -1687,22 +1686,22 @@

      Inherited members

      # when the wheel factor is a character, the filter is a spectral filter # will be late replaced by the setup configuration self.wheel1 = {0: 0, - 1: 2, - 2: 1, - 3: 0.03, - 4: "T4K", - 5: "T5K", - 6: "T6K", - 7: "T6.5K"} + 1: 2, + 2: 1, + 3: 0.03, + 4: "T4K", + 5: "T5K", + 6: "T6K", + 7: "T6.5K"} self.wheel2 = {0: 0, - 1: 0.3, - 2: 0.05, - 3: 5, - 4: 2, - 5: 0.1, - 6: 0.15, - 7: 3} + 1: 0.3, + 2: 0.05, + 3: 5, + 4: 2, + 5: 0.1, + 6: 0.15, + 7: 3} self._setup = None self._att_level = [self.wheel1, self.wheel2, 0] @@ -1736,7 +1735,6 @@

      Inherited members

      "Current Motor 2": 0, "Current Motor 3": 0} - def is_connected(self): return True @@ -1753,8 +1751,7 @@

      Inherited members

      self.connect() def load_wheels(self): - with ConfigurationManagerProxy() as cm: - self._setup = cm.get_setup() + self._setup = load_setup() #todo: still some work here to manage the default setup to be loaded return self._setup diff --git a/docs/api/egse/filterwheel/eksma/fw8smc4_devif.html b/docs/api/egse/filterwheel/eksma/fw8smc4_devif.html index e478a33..fab5e4b 100644 --- a/docs/api/egse/filterwheel/eksma/fw8smc4_devif.html +++ b/docs/api/egse/filterwheel/eksma/fw8smc4_devif.html @@ -158,16 +158,7 @@

      Module egse.filterwheel.eksma.fw8smc4_devif

      f"{x_device_information.Minor!r}." f"{x_device_information.Release!r}" ) - # version = repr(x_device_information.Major) + "." + \ - # repr(x_device_information.Minor) + "." + \ - # repr(x_device_information.Release) description = f"{string_at(x_device_information.ProductDescription).decode()!r}" - logger.debug( - f"Device information:Product Description: {description}, version: {version}" - ) - # logger.info("\nDevice information:" + "\nProduct Description:" + - # repr(string_at(x_device_information.ProductDescription).decode()) + - # "\nVersion: " + version) device_id = { "Product Description": description, "Version": version @@ -191,26 +182,24 @@

      Module egse.filterwheel.eksma.fw8smc4_devif

      self.move_wheel(9) self.command_zero() - logging.info(f"New zero position reached:({self.get_position()[0]})") - logging.info(f"Commanding the wheels to the equalized position:({position})") - + self.move_wheel(position+1) - logging.info(f"New equalized position reached at:({self.get_position()[0]})") def move_wheel(self, steps): - print("moving wheel to the following number of x25 steps:", steps) steps = steps - 1 try: ximc.command_move(self._devId, steps*25, 0) - time.sleep(0.3) - while self.is_moving() != 0: - logger.info("The wheel is moving ...") - time.sleep(0.03) - logger.info("... the wheel stopped its movement") + time.sleep(2) + while self.get_speed() != 0: + time.sleep(1) except AssertionError: logger.warning("Something went wrong when moving the filterwheel") def set_pos(self, pos_wheel2, pos_wheel1): + self.homing() + while self.get_speed() != 0: + time.sleep(1) + # delay so the commands don't overlap # the wheel 2 is the one that moves (opposite side of the motor) # gets the actual position @@ -222,7 +211,6 @@

      Module egse.filterwheel.eksma.fw8smc4_devif

      # 1 turn of the wheel has 200 steps, so each transition is reached by 25 steps. if pos_wheel1 == pos_wheel2: self.equalize_wheels(pos_wheel2) - logger.info("position 1&2 reached") elif pos_wheel1 < pos_wheel2: if abs(pos) < pos_wheel1*25: @@ -230,9 +218,7 @@

      Module egse.filterwheel.eksma.fw8smc4_devif

      else: self.equalize_wheels(pos_wheel1) - logger.info("position 1 reached") self.move_wheel(pos_wheel2-7) - logger.info("position 2 reached") elif pos_wheel1 > pos_wheel2: @@ -241,41 +227,27 @@

      Module egse.filterwheel.eksma.fw8smc4_devif

      else: self.equalize_wheels(pos_wheel1) - logger.info("position 1 reached") self.move_wheel(pos_wheel2+1) - logger.info("position 2 reached") - _output = self.wait_stop(10) - if _output == 0: - logger.info("Movement finished") - if _output > 0: - logger.warning("Movement finished after forcing the wait_stop") - if _output < 0: - logger.error("Error on movement") - return _output + + return 0 def command_zero(self): logger.warning("Commanding the wheels to zero position") try: ximc.command_zero(self._devId) - time.sleep(0.3) - while self.is_moving() != 0: - logger.info("The wheel is moving to zero...") + while self.get_speed() != 0: time.sleep(0.03) - logger.info("... the wheel stopped its movement") except AssertionError: logger.warning("Something went wrong when moving the filterwheel") def homing(self): - logger.info("Starting homing sequence ...") - + logger.info("Starting homing sequence") ximc.command_homezero(self._devId) - logger.info("... Wheels homing succeeded") def get_status(self): status = status_t() - ximc.get_status(self._devId, byref(status)) return status @@ -284,30 +256,6 @@

      Module egse.filterwheel.eksma.fw8smc4_devif

      _speed = self.get_status() return _speed.CurSpeed - #fixme: this is not working, so don't use it for the moment. Replaced with time.sleeps - def wait_stop(self, t): - _output = None - timeout = time.time() + t - time.sleep(0.3) - _speed = self.get_speed() - while time.time() < timeout: - time.sleep(0.03) - _speed = self.get_speed() - if _speed == 0: - break - if _speed != 0: - logger.warning("wait_stop timeout over but the motor is still running, forcing the wheel to stop") - result = lib.command_wait_for_stop(self._devId, 10) - if result.Result == Ok: - logger.warning("The wheel stopped after forcing command") - _output = 1 - else: - logger.error("An error occurred during the movement") - _output = -1 - else: - _output = 0 - return _output - def get_flags(self): status = self.get_status() return (status.Flags) @@ -320,6 +268,7 @@

      Module egse.filterwheel.eksma.fw8smc4_devif

      + class FilterWheel8SMC4EthernetInterface: def __init__(self): @@ -391,12 +340,6 @@

      Module egse.filterwheel.eksma.fw8smc4_devif

      # repr(x_device_information.Minor) + "." + \ # repr(x_device_information.Release) description = f"{string_at(x_device_information.ProductDescription).decode()!r}" - logger.debug( - f"Device information:Product Description: {description}, version: {version}" - ) - # logger.info("\nDevice information:" + "\nProduct Description:" + - # repr(string_at(x_device_information.ProductDescription).decode()) + - # "\nVersion: " + version) device_id = { "Product Description": description, "Version": version @@ -432,14 +375,15 @@

      Module egse.filterwheel.eksma.fw8smc4_devif

      try: lib.command_move(self._fw, steps*25, 0) time.sleep(0.3) + logger.info("The wheel is moving ...") while self.is_moving() != 0: - logger.info("The wheel is moving ...") - time.sleep(0.03) + time.sleep(0.3) logger.info("... the wheel stopped its movement") except AssertionError: logger.warning("Something went wrong when moving the filterwheel") def set_pos(self, pos_wheel1, pos_wheel2): + self.homing() # delay so the commands don't overlap # the wheel 2 is the one that moves (opposite side of the motor) # gets the actual position @@ -451,7 +395,7 @@

      Module egse.filterwheel.eksma.fw8smc4_devif

      # 1 turn of the wheel has 200 steps, so each transition is reached by 25 steps. if pos_wheel1 == pos_wheel2: self.equalize_wheels(pos_wheel2) - logger.info("position 1&2 reached") + logger.info("Positions reached for both wheels") elif pos_wheel1 < pos_wheel2: if abs(pos) < pos_wheel1*25: @@ -488,9 +432,9 @@

      Module egse.filterwheel.eksma.fw8smc4_devif

      try: lib.command_zero(self._fw) time.sleep(0.3) + logger.info("The wheel is moving to zero...") while self.is_moving() != 0: - logger.info("The wheel is moving to zero...") - time.sleep(0.03) + time.sleep(0.3) logger.info("... the wheel stopped its movement") except AssertionError: logger.warning("Something went wrong when moving the filterwheel") @@ -718,12 +662,6 @@

      Ancestors

      # repr(x_device_information.Minor) + "." + \ # repr(x_device_information.Release) description = f"{string_at(x_device_information.ProductDescription).decode()!r}" - logger.debug( - f"Device information:Product Description: {description}, version: {version}" - ) - # logger.info("\nDevice information:" + "\nProduct Description:" + - # repr(string_at(x_device_information.ProductDescription).decode()) + - # "\nVersion: " + version) device_id = { "Product Description": description, "Version": version @@ -759,14 +697,15 @@

      Ancestors

      try: lib.command_move(self._fw, steps*25, 0) time.sleep(0.3) + logger.info("The wheel is moving ...") while self.is_moving() != 0: - logger.info("The wheel is moving ...") - time.sleep(0.03) + time.sleep(0.3) logger.info("... the wheel stopped its movement") except AssertionError: logger.warning("Something went wrong when moving the filterwheel") def set_pos(self, pos_wheel1, pos_wheel2): + self.homing() # delay so the commands don't overlap # the wheel 2 is the one that moves (opposite side of the motor) # gets the actual position @@ -778,7 +717,7 @@

      Ancestors

      # 1 turn of the wheel has 200 steps, so each transition is reached by 25 steps. if pos_wheel1 == pos_wheel2: self.equalize_wheels(pos_wheel2) - logger.info("position 1&2 reached") + logger.info("Positions reached for both wheels") elif pos_wheel1 < pos_wheel2: if abs(pos) < pos_wheel1*25: @@ -815,9 +754,9 @@

      Ancestors

      try: lib.command_zero(self._fw) time.sleep(0.3) + logger.info("The wheel is moving to zero...") while self.is_moving() != 0: - logger.info("The wheel is moving to zero...") - time.sleep(0.03) + time.sleep(0.3) logger.info("... the wheel stopped its movement") except AssertionError: logger.warning("Something went wrong when moving the filterwheel") @@ -899,9 +838,9 @@

      Methods

      try: lib.command_zero(self._fw) time.sleep(0.3) + logger.info("The wheel is moving to zero...") while self.is_moving() != 0: - logger.info("The wheel is moving to zero...") - time.sleep(0.03) + time.sleep(0.3) logger.info("... the wheel stopped its movement") except AssertionError: logger.warning("Something went wrong when moving the filterwheel") @@ -1017,12 +956,6 @@

      Methods

      # repr(x_device_information.Minor) + "." + \ # repr(x_device_information.Release) description = f"{string_at(x_device_information.ProductDescription).decode()!r}" - logger.debug( - f"Device information:Product Description: {description}, version: {version}" - ) - # logger.info("\nDevice information:" + "\nProduct Description:" + - # repr(string_at(x_device_information.ProductDescription).decode()) + - # "\nVersion: " + version) device_id = { "Product Description": description, "Version": version @@ -1180,9 +1113,9 @@

      Returns

      try: lib.command_move(self._fw, steps*25, 0) time.sleep(0.3) + logger.info("The wheel is moving ...") while self.is_moving() != 0: - logger.info("The wheel is moving ...") - time.sleep(0.03) + time.sleep(0.3) logger.info("... the wheel stopped its movement") except AssertionError: logger.warning("Something went wrong when moving the filterwheel") @@ -1198,6 +1131,7 @@

      Returns

      Expand source code
      def set_pos(self, pos_wheel1, pos_wheel2):
      +    self.homing()
           # delay so the commands don't overlap
           # the wheel 2 is the one that moves (opposite side of the motor)
           # gets the actual position
      @@ -1209,7 +1143,7 @@ 

      Returns

      # 1 turn of the wheel has 200 steps, so each transition is reached by 25 steps. if pos_wheel1 == pos_wheel2: self.equalize_wheels(pos_wheel2) - logger.info("position 1&2 reached") + logger.info("Positions reached for both wheels") elif pos_wheel1 < pos_wheel2: if abs(pos) < pos_wheel1*25: @@ -1339,16 +1273,7 @@

      Returns

      f"{x_device_information.Minor!r}." f"{x_device_information.Release!r}" ) - # version = repr(x_device_information.Major) + "." + \ - # repr(x_device_information.Minor) + "." + \ - # repr(x_device_information.Release) description = f"{string_at(x_device_information.ProductDescription).decode()!r}" - logger.debug( - f"Device information:Product Description: {description}, version: {version}" - ) - # logger.info("\nDevice information:" + "\nProduct Description:" + - # repr(string_at(x_device_information.ProductDescription).decode()) + - # "\nVersion: " + version) device_id = { "Product Description": description, "Version": version @@ -1372,26 +1297,24 @@

      Returns

      self.move_wheel(9) self.command_zero() - logging.info(f"New zero position reached:({self.get_position()[0]})") - logging.info(f"Commanding the wheels to the equalized position:({position})") - + self.move_wheel(position+1) - logging.info(f"New equalized position reached at:({self.get_position()[0]})") def move_wheel(self, steps): - print("moving wheel to the following number of x25 steps:", steps) steps = steps - 1 try: ximc.command_move(self._devId, steps*25, 0) - time.sleep(0.3) - while self.is_moving() != 0: - logger.info("The wheel is moving ...") - time.sleep(0.03) - logger.info("... the wheel stopped its movement") + time.sleep(2) + while self.get_speed() != 0: + time.sleep(1) except AssertionError: logger.warning("Something went wrong when moving the filterwheel") def set_pos(self, pos_wheel2, pos_wheel1): + self.homing() + while self.get_speed() != 0: + time.sleep(1) + # delay so the commands don't overlap # the wheel 2 is the one that moves (opposite side of the motor) # gets the actual position @@ -1403,7 +1326,6 @@

      Returns

      # 1 turn of the wheel has 200 steps, so each transition is reached by 25 steps. if pos_wheel1 == pos_wheel2: self.equalize_wheels(pos_wheel2) - logger.info("position 1&2 reached") elif pos_wheel1 < pos_wheel2: if abs(pos) < pos_wheel1*25: @@ -1411,9 +1333,7 @@

      Returns

      else: self.equalize_wheels(pos_wheel1) - logger.info("position 1 reached") self.move_wheel(pos_wheel2-7) - logger.info("position 2 reached") elif pos_wheel1 > pos_wheel2: @@ -1422,41 +1342,27 @@

      Returns

      else: self.equalize_wheels(pos_wheel1) - logger.info("position 1 reached") self.move_wheel(pos_wheel2+1) - logger.info("position 2 reached") - _output = self.wait_stop(10) - if _output == 0: - logger.info("Movement finished") - if _output > 0: - logger.warning("Movement finished after forcing the wait_stop") - if _output < 0: - logger.error("Error on movement") - return _output + + return 0 def command_zero(self): logger.warning("Commanding the wheels to zero position") try: ximc.command_zero(self._devId) - time.sleep(0.3) - while self.is_moving() != 0: - logger.info("The wheel is moving to zero...") + while self.get_speed() != 0: time.sleep(0.03) - logger.info("... the wheel stopped its movement") except AssertionError: logger.warning("Something went wrong when moving the filterwheel") def homing(self): - logger.info("Starting homing sequence ...") - + logger.info("Starting homing sequence") ximc.command_homezero(self._devId) - logger.info("... Wheels homing succeeded") def get_status(self): status = status_t() - ximc.get_status(self._devId, byref(status)) return status @@ -1465,30 +1371,6 @@

      Returns

      _speed = self.get_status() return _speed.CurSpeed - #fixme: this is not working, so don't use it for the moment. Replaced with time.sleeps - def wait_stop(self, t): - _output = None - timeout = time.time() + t - time.sleep(0.3) - _speed = self.get_speed() - while time.time() < timeout: - time.sleep(0.03) - _speed = self.get_speed() - if _speed == 0: - break - if _speed != 0: - logger.warning("wait_stop timeout over but the motor is still running, forcing the wheel to stop") - result = lib.command_wait_for_stop(self._devId, 10) - if result.Result == Ok: - logger.warning("The wheel stopped after forcing command") - _output = 1 - else: - logger.error("An error occurred during the movement") - _output = -1 - else: - _output = 0 - return _output - def get_flags(self): status = self.get_status() return (status.Flags) @@ -1512,11 +1394,8 @@

      Methods

      logger.warning("Commanding the wheels to zero position") try: ximc.command_zero(self._devId) - time.sleep(0.3) - while self.is_moving() != 0: - logger.info("The wheel is moving to zero...") + while self.get_speed() != 0: time.sleep(0.03) - logger.info("... the wheel stopped its movement") except AssertionError: logger.warning("Something went wrong when moving the filterwheel")
      @@ -1579,11 +1458,8 @@

      Methods

      self.move_wheel(9) self.command_zero() - logging.info(f"New zero position reached:({self.get_position()[0]})") - logging.info(f"Commanding the wheels to the equalized position:({position})") - - self.move_wheel(position+1) - logging.info(f"New equalized position reached at:({self.get_position()[0]})") + + self.move_wheel(position+1)
    @@ -1619,16 +1495,7 @@

    Methods

    f"{x_device_information.Minor!r}." f"{x_device_information.Release!r}" ) - # version = repr(x_device_information.Major) + "." + \ - # repr(x_device_information.Minor) + "." + \ - # repr(x_device_information.Release) description = f"{string_at(x_device_information.ProductDescription).decode()!r}" - logger.debug( - f"Device information:Product Description: {description}, version: {version}" - ) - # logger.info("\nDevice information:" + "\nProduct Description:" + - # repr(string_at(x_device_information.ProductDescription).decode()) + - # "\nVersion: " + version) device_id = { "Product Description": description, "Version": version @@ -1692,7 +1559,6 @@

    Methods

    def get_status(self):
         status = status_t()
    -
         ximc.get_status(self._devId, byref(status))
     
         return status
    @@ -1708,11 +1574,8 @@

    Methods

    Expand source code
    def homing(self):
    -    logger.info("Starting homing sequence ...")
    -
    -    ximc.command_homezero(self._devId)
    -
    -    logger.info("... Wheels homing succeeded")
    + logger.info("Starting homing sequence") + ximc.command_homezero(self._devId)
    @@ -1752,15 +1615,12 @@

    Methods

    Expand source code
    def move_wheel(self, steps):
    -    print("moving wheel to the following number of x25 steps:", steps)
         steps = steps - 1
         try:
             ximc.command_move(self._devId, steps*25, 0)
    -        time.sleep(0.3)
    -        while self.is_moving() != 0:
    -            logger.info("The wheel is moving ...")
    -            time.sleep(0.03)
    -        logger.info("... the wheel stopped its movement")
    +        time.sleep(2)
    +        while self.get_speed() != 0:
    +            time.sleep(1)
         except AssertionError:
             logger.warning("Something went wrong when moving the filterwheel")
    @@ -1775,6 +1635,10 @@

    Methods

    Expand source code
    def set_pos(self, pos_wheel2, pos_wheel1):
    +    self.homing()
    +    while self.get_speed() != 0:
    +        time.sleep(1)
    +
         # delay so the commands don't overlap
         # the wheel 2 is the one that moves (opposite side of the motor)
         # gets the actual position
    @@ -1786,7 +1650,6 @@ 

    Methods

    # 1 turn of the wheel has 200 steps, so each transition is reached by 25 steps. if pos_wheel1 == pos_wheel2: self.equalize_wheels(pos_wheel2) - logger.info("position 1&2 reached") elif pos_wheel1 < pos_wheel2: if abs(pos) < pos_wheel1*25: @@ -1794,9 +1657,7 @@

    Methods

    else: self.equalize_wheels(pos_wheel1) - logger.info("position 1 reached") self.move_wheel(pos_wheel2-7) - logger.info("position 2 reached") elif pos_wheel1 > pos_wheel2: @@ -1805,51 +1666,10 @@

    Methods

    else: self.equalize_wheels(pos_wheel1) - logger.info("position 1 reached") self.move_wheel(pos_wheel2+1) - logger.info("position 2 reached") - _output = self.wait_stop(10) - if _output == 0: - logger.info("Movement finished") - if _output > 0: - logger.warning("Movement finished after forcing the wait_stop") - if _output < 0: - logger.error("Error on movement") - return _output
    - - -
    -def wait_stop(self, t) -
    -
    -
    -
    - -Expand source code - -
    def wait_stop(self, t):
    -    _output = None
    -    timeout = time.time() + t
    -    time.sleep(0.3)
    -    _speed = self.get_speed()
    -    while time.time() < timeout:
    -        time.sleep(0.03)
    -        _speed = self.get_speed()
    -        if _speed == 0:
    -            break
    -    if _speed != 0:
    -        logger.warning("wait_stop timeout over but the motor is still running, forcing the wheel to stop")
    -        result = lib.command_wait_for_stop(self._devId, 10)
    -        if result.Result == Ok:
    -            logger.warning("The wheel stopped after forcing command")
    -            _output = 1
    -        else:
    -            logger.error("An error occurred during the movement")
    -            _output = -1
    -    else:
    -        _output = 0
    -    return _output
    + + return 0
    @@ -1918,7 +1738,6 @@

    is_moving
  • move_wheel
  • set_pos
  • -
  • wait_stop
  • diff --git a/docs/api/egse/filterwheel/eksma/fw8smc4_protocol.html b/docs/api/egse/filterwheel/eksma/fw8smc4_protocol.html index a37adb5..22253e3 100644 --- a/docs/api/egse/filterwheel/eksma/fw8smc4_protocol.html +++ b/docs/api/egse/filterwheel/eksma/fw8smc4_protocol.html @@ -27,6 +27,7 @@

    Module egse.filterwheel.eksma.fw8smc4_protocolExpand source code
    import logging
    +from prometheus_client import Gauge
     
     from egse.control import ControlServer
     from egse.metrics import define_metrics
    @@ -66,7 +67,7 @@ 

    Module egse.filterwheel.eksma.fw8smc4_protocolModule egse.filterwheel.eksma.fw8smc4_protocolClasses

    # self.fwc_calibration = GlobalState.setup.gse.ogse.calibration.fwc_calibration TODO self.synoptics = SynopticsManagerProxy() - self.metrics = define_metrics("FW8SMC4") + self.metrics = define_metrics(origin="FW8SMC4", use_site=True) def get_bind_address(self): return bind_address( @@ -161,21 +167,26 @@

    Classes

    def get_housekeeping(self) -> dict: - + metrics_dict = self.metrics hk_dict = dict() hk_dict["timestamp"] = format_datetime() try: - filterwheel_positions = self.filterwheel.get_position() # relative_intensity = self.filterwheel.get_relative_intensity() # TODO Use the correct command name # hk_dict["G{SITE_ID}_FW8SMC4_REL_INTENSITY"] = relative_intensity # TODO # hk_dict["G{SITE_ID}_FW8SMC4_FWC_FRACTION"] = relative_intensity / self.fwc_calibration, # TODO - + # self.synoptics.store_th_synoptics(hk_dict) TODO Uncomment when the relative intensity + FWC fraction are available # TODO Update the TM dictionary (entry for the HK + for the synoptics) - hk_dict[f"G{SITE_ID}_FW8SMC4_POS_0"] = filterwheel_positions[0] - hk_dict[f"G{SITE_ID}_FW8SMC4_POS_1"] = filterwheel_positions[1] + filterwheel_motor_positions = self.filterwheel.get_position() + filterwheel_status = self.filterwheel.get_status() + + hk_values = [filterwheel_motor_positions[0], filterwheel_motor_positions[1], filterwheel_status[2], filterwheel_status[3], filterwheel_status[4]] + hk_dict.update({hk_name: hk_value for hk_name, hk_value in zip(metrics_dict.keys(), hk_values)}) + + for hk_name in metrics_dict.keys(): + metrics_dict[hk_name].set(hk_dict[hk_name]) # Send the HK acquired so far to the Synoptics Manager self.synoptics.store_th_synoptics(hk_dict) diff --git a/docs/api/egse/filterwheel/eksma/fw8smc4_ui.html b/docs/api/egse/filterwheel/eksma/fw8smc4_ui.html index 7d94d25..4735a02 100644 --- a/docs/api/egse/filterwheel/eksma/fw8smc4_ui.html +++ b/docs/api/egse/filterwheel/eksma/fw8smc4_ui.html @@ -1754,7 +1754,7 @@

    Methods

    class FilterWheelUIView
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code diff --git a/docs/api/egse/filterwheel/eksma/fw8smc5.html b/docs/api/egse/filterwheel/eksma/fw8smc5.html index abd8652..f084797 100644 --- a/docs/api/egse/filterwheel/eksma/fw8smc5.html +++ b/docs/api/egse/filterwheel/eksma/fw8smc5.html @@ -35,12 +35,11 @@

    Module egse.filterwheel.eksma.fw8smc5

    from egse.command import ClientServerCommand from egse.proxy import Proxy from egse.settings import Settings -from egse.state import GlobalState +from egse.setup import load_setup from egse.synoptics import SynopticsManagerProxy from egse.zmq_ser import bind_address from egse.system import format_datetime from egse.filterwheel.eksma.fw8smc5_interface import Fw8Smc5Interface -from egse.filterwheel.eksma.fw8smc5_controller import Fw8Smc5Controller from egse.filterwheel.eksma.fw8smc5_simulator import Fw8Smc5Simulator from egse.zmq_ser import connect_address @@ -65,10 +64,15 @@

    Module egse.filterwheel.eksma.fw8smc5

    def __init__(self, control_server: ControlServer): + from egse.filterwheel.eksma.fw8smc5_controller import Fw8Smc5Controller + super().__init__() + + setup = load_setup() + self.control_server = control_server - self.fwc_calibration = GlobalState.setup.gse.ogse.calibration.fwc_calibration + self.fwc_calibration = setup.gse.ogse.calibration.fwc_calibration if Settings.simulation_mode(): self.dev = Fw8Smc5Simulator() @@ -202,10 +206,15 @@

    Inherited members

    def __init__(self, control_server: ControlServer): + from egse.filterwheel.eksma.fw8smc5_controller import Fw8Smc5Controller + super().__init__() + + setup = load_setup() + self.control_server = control_server - self.fwc_calibration = GlobalState.setup.gse.ogse.calibration.fwc_calibration + self.fwc_calibration = setup.gse.ogse.calibration.fwc_calibration if Settings.simulation_mode(): self.dev = Fw8Smc5Simulator() @@ -324,7 +333,7 @@

    Ancestors

  • Proxy
  • BaseProxy
  • ControlServerConnectionInterface
  • -
  • Fw8Smc5Interface
  • +
  • egse.filterwheel.eksma.fw8smc5_interface.Fw8Smc5Interface
  • DeviceInterface
  • DeviceConnectionInterface
  • DeviceConnectionObservable
  • @@ -350,15 +359,15 @@

    Inherited members

  • send
  • -
  • Fw8Smc5Interface: +
  • DeviceInterface:
  • diff --git a/docs/api/egse/filterwheel/eksma/fw8smc5_cs.html b/docs/api/egse/filterwheel/eksma/fw8smc5_cs.html index de2b2c1..34ca189 100644 --- a/docs/api/egse/filterwheel/eksma/fw8smc5_cs.html +++ b/docs/api/egse/filterwheel/eksma/fw8smc5_cs.html @@ -344,10 +344,15 @@

    Inherited members

    Subclasses

    Methods

    diff --git a/docs/api/egse/filterwheel/eksma/fw8smc5_simulator.html b/docs/api/egse/filterwheel/eksma/fw8smc5_simulator.html index 5c104b7..1522167 100644 --- a/docs/api/egse/filterwheel/eksma/fw8smc5_simulator.html +++ b/docs/api/egse/filterwheel/eksma/fw8smc5_simulator.html @@ -94,23 +94,42 @@

    Classes

    Ancestors

    +

    Methods

    +
    +
    +def is_simulator(self) +
    +
    +

    Ask if the device class is a Simulator instead of the real Controller.

    +

    This can be useful for testing purposes or when doing actual movement simulations.

    +

    Returns

    +

    True if the Device is a Simulator, False if the Device is connected to real hardware.

    +
    + +Expand source code + +
    def is_simulator(self):
    +
    +    return True
    +
    +
    +

    Inherited members

    @@ -133,6 +152,9 @@

    Index

    diff --git a/docs/api/egse/filterwheel/eksma/fw8smc5_ui.html b/docs/api/egse/filterwheel/eksma/fw8smc5_ui.html index 30f36a5..e138013 100644 --- a/docs/api/egse/filterwheel/eksma/fw8smc5_ui.html +++ b/docs/api/egse/filterwheel/eksma/fw8smc5_ui.html @@ -41,12 +41,10 @@

    Module egse.filterwheel.eksma.fw8smc5_ui

    from zmq import ZMQError from egse.filterwheel.eksma.fw8smc5 import CTRL_SETTINGS as FW_CTRL_SETTINGS -from egse.filterwheel.eksma.fw8smc5 import Fw8Smc5Proxy, Fw8Smc5Simulator +from egse.filterwheel.eksma.fw8smc5 import Fw8Smc5Proxy from egse.gui.led import Led from egse.lampcontrol.beaglebone.beaglebone import BeagleboneProxy from egse.lampcontrol.beaglebone.beaglebone import CTRL_SETTINGS as BB_CTRL_SETTINGS - -from egse.gui.led import Led, Indic as LedIndic from egse.observer import Observer, Observable from egse.powermeter.thorlabs.pm100a import CTRL_SETTINGS as PM100_CTRL_SETTINGS from egse.resource import get_resource @@ -55,7 +53,6 @@

    Module egse.filterwheel.eksma.fw8smc5_ui

    from egse.shutter.thorlabs.sc10 import Sc10Proxy from egse.stages.arun.smd3 import CTRL_SETTINGS as SMD3_CTRL_SETTINGS from egse.stages.arun.smd3 import Smd3Proxy - from egse.zmq_ser import connect_address multiprocessing.current_process().name = 'fw8smc5_ui' @@ -701,6 +698,7 @@

    Module egse.filterwheel.eksma.fw8smc5_ui

    self.groupbox.pushButton_29.clicked.connect(self.toggle_lamp_control) self.groupbox.pushButton_30.clicked.connect(self.toggle_lamp_control) + self.groupbox.pushButton_31.clicked.connect(self.clear_lamp_fault) self.groupbox.lineEdit_6.textChanged.connect(self.update_scrollArea) @@ -803,6 +801,12 @@

    Module egse.filterwheel.eksma.fw8smc5_ui

    except Exception as e: warning_popup(f"set_lamp {state}", e) + def clear_lamp_fault(self): + try: + self.actionObservers({'fix_controller_fault': []}) + except Exception as e: + warning_popup("fix_controller_fault", e) + def update_scrollArea(self, text): for button in self.groupbox.ri_pushButtons.values(): if text in button.text(): @@ -968,6 +972,10 @@

    Module egse.filterwheel.eksma.fw8smc5_ui

    def set_lamp(self, state): with BeagleboneProxy() as bb: bb.set_lamp(state) + + def fix_controller_fault(self): + with BeagleboneProxy() as bb: + bb.fix_controller_fault() class OGSEUIController(Observer): @@ -1034,6 +1042,8 @@

    Module egse.filterwheel.eksma.fw8smc5_ui

    if action == "set_lamp": state = value[0] self.model.set_lamp(state) + if action == "fix_controller_fault": + self.model.fix_controller_fault() def parse_arguments(): """ @@ -1180,7 +1190,7 @@

    Classes

    class FilterWheelMonitoringWorker
    -

    QObject(parent: QObject = None)

    +

    QObject(parent: typing.Optional[QObject] = None)

    Expand source code @@ -1755,7 +1765,9 @@

    Raises

    self.model.home_filterwheel() if action == "set_lamp": state = value[0] - self.model.set_lamp(state)
    + self.model.set_lamp(state) + if action == "fix_controller_fault": + self.model.fix_controller_fault()

    Ancestors

    @@ -72,18 +97,6 @@

    Sub-modules

    This module defines the basic classes to access the EKSMA Filter wheel connected to the 8SMC4 motor controller that will be used in the IAS TVAC …

    -
    egse.filterwheel.eksma.fw8smc4_cs
    -
    -
    -
    -
    egse.filterwheel.eksma.fw8smc4_devif
    -
    -
    -
    -
    egse.filterwheel.eksma.fw8smc4_protocol
    -
    -
    -
    egse.filterwheel.eksma.fw8smc4_ui
    @@ -92,18 +105,10 @@

    Sub-modules

    -
    egse.filterwheel.eksma.fw8smc5_controller
    -
    -
    -
    egse.filterwheel.eksma.fw8smc5_cs
    -
    egse.filterwheel.eksma.fw8smc5_interface
    -
    -
    -
    egse.filterwheel.eksma.fw8smc5_simulator
    @@ -112,10 +117,6 @@

    Sub-modules

    -
    egse.filterwheel.eksma.testpythonfw
    -
    -
    -
    @@ -139,17 +140,11 @@

    Index

  • Sub-modules

  • diff --git a/docs/api/egse/fov/fov_hk.html b/docs/api/egse/fov/fov_hk.html index da94f5a..eccc71d 100644 --- a/docs/api/egse/fov/fov_hk.html +++ b/docs/api/egse/fov/fov_hk.html @@ -33,17 +33,17 @@

    Module egse.fov.fov_hk

    import click import invoke +import numpy as np import rich import sys import zmq -from math import sin, cos, tan, acos, asin, atan, atan2, radians, degrees, pi, sqrt -from numpy import sign +from math import sin, cos, acos, atan, atan2, radians, degrees from prometheus_client import start_http_server from egse.control import is_control_server_active from egse.fov import store_actual_fov_position from egse.settings import Settings -from egse.state import GlobalState +from egse.setup import load_setup from egse.storage import is_storage_manager_active from egse.storage import register_to_storage_manager from egse.storage.persistence import CSV @@ -137,7 +137,7 @@

    Module egse.fov.fov_hk

    def __init__(self): - self.setup = GlobalState.setup + self.setup = load_setup() self.keep_acquiring_fov_hk = True def run(self, commander, poller): @@ -182,7 +182,7 @@

    Module egse.fov.fov_hk

    super(CslFovHk, self).__init__() - setup = GlobalState.setup + setup = load_setup() self.height_collimated_beam = setup.gse.stages.calibration.height_collimated_beam self.offset_delta_x = setup.gse.stages.calibration.offset_delta_x self.offset_phi = setup.gse.stages.calibration.offset_phi @@ -262,7 +262,7 @@

    Module egse.fov.fov_hk

    super(Csl1FovHk, self).__init__() - setup = GlobalState.setup + setup = load_setup() self.offset_alpha = setup.gse.stages.calibration.offset_alpha self.alpha_correction_coefficients = setup.gse.stages.calibration.alpha_correction_coefficients self.offset_phi = setup.gse.stages.calibration.offset_phi @@ -401,27 +401,29 @@

    Module egse.fov.fov_hk

    rotx = radians(monitoring_info["alpha"]) # [radians] roty = radians(monitoring_info["beta"]) # [radians] + if isinstance(roty, float): + rotx = np.array([rotx]) + roty = np.array([roty]) + tolerance = 1.e-5 - # Gnomonic distance to optical axis and in-field angle [degrees] + # Corrections based on #598 + # Copied from camtest.commanding.function.sron_functions.gimbal_rotations_to_fov_angles + theta = np.arccos(np.cos(rotx) * np.cos(roty)) - theta = degrees(acos(cos(rotx) * cos(roty))) # [degrees] + phi = np.zeros_like(roty) - phi = 0.0 - if abs(roty) < tolerance: - phi = sign(rotx) * pi / 2. - elif abs(roty) > tolerance: - phi = atan(sign(roty) * tan(rotx) / sin(roty)) + sel = np.where(np.abs(roty) < tolerance) - phi = degrees(phi) # [degrees] + phi[sel] = np.sign(rotx[sel]) * np.pi/2. - if roty < 0: - phi = -180. - phi + sel = np.where(np.abs(roty) > tolerance) - if phi < -180: - phi += 360. + phi[sel] = np.arctan2(-np.sin(-rotx[sel]) * np.cos(roty[sel]), np.sin(roty[sel])) - store_actual_fov_position(theta, phi) + theta, phi = np.rad2deg(theta), np.rad2deg(phi) + + store_actual_fov_position(theta[0], phi[0]) elif is_control_server_active(self.cmd_endpoint): self.monitoring_timeout += 0.5 @@ -488,13 +490,20 @@

    Module egse.fov.fov_hk

    # Retrieve the rotation angles around the y- and z-axis [degrees] # (the rotation angle around the x-axis and the translation are not needed here) + rot_y_z_mini = 1 / 3600 # lower threshold: 1 arcsecond = 1/3600 degree + *_, hexapod_rot_y, hexapod_rot_z = monitoring_info["user"] - hexapod_rot_y = radians(hexapod_rot_y) # Rotation angle around the x-axis [radians] - hexapod_rot_z = radians(hexapod_rot_z) # Rotation angle around the y-axis [radians] + if hexapod_rot_y < rot_y_z_mini and hexapod_rot_z < rot_y_z_mini: + theta = 0 + phi = 0 - theta = degrees(acos(cos(hexapod_rot_y) * cos(hexapod_rot_z))) - phi = degrees(-asin(sin(hexapod_rot_z) / sqrt(1 - pow(cos(hexapod_rot_y) * cos(hexapod_rot_z), 2)))) + else: + hexapod_rot_y = radians(hexapod_rot_y) # Rotation angle around the x-axis [radians] + hexapod_rot_z = radians(hexapod_rot_z) # Rotation angle around the y-axis [radians] + + theta = degrees(acos(cos(hexapod_rot_y) * cos(hexapod_rot_z))) + phi = degrees(atan2(sin(hexapod_rot_z), -sin(hexapod_rot_y) * cos(hexapod_rot_z))) store_actual_fov_position(theta, phi) @@ -572,13 +581,14 @@

    Module egse.fov.fov_hk

    # # According to inta_point_source_to_fov: - # grx = theta * np.cos(np.deg2rad(phi)) - # gry = theta * np.sin(np.deg2rad(phi)) + # grx = np.rad2deg(-np.arcsin(np.sin(np.deg2rad(theta)) * np.sin(np.deg2rad(phi)))) + # gry = np.rad2deg(np.arctan2(np.sin(np.deg2rad(theta)) * np.cos(np.deg2rad(phi)), np.cos(np.deg2rad(theta)))) # # grx and gry are already in degrees, therefore: # - theta = sqrt(grx ** 2 + gry ** 2) - phi = degrees(atan2(gry, grx)) + + theta = np.rad2deg(np.arccos(np.cos(np.deg2rad(grx))*np.cos(np.deg2rad(gry)))) + phi = np.rad2deg(np.arctan2(-np.sin(np.deg2rad(grx)), np.cos(np.deg2rad(grx))*np.sin(np.deg2rad(gry)))) store_actual_fov_position(theta, phi) @@ -796,7 +806,7 @@

    Classes

    super(Csl1FovHk, self).__init__() - setup = GlobalState.setup + setup = load_setup() self.offset_alpha = setup.gse.stages.calibration.offset_alpha self.alpha_correction_coefficients = setup.gse.stages.calibration.alpha_correction_coefficients self.offset_phi = setup.gse.stages.calibration.offset_phi @@ -938,7 +948,7 @@

    Methods

    super(CslFovHk, self).__init__() - setup = GlobalState.setup + setup = load_setup() self.height_collimated_beam = setup.gse.stages.calibration.height_collimated_beam self.offset_delta_x = setup.gse.stages.calibration.offset_delta_x self.offset_phi = setup.gse.stages.calibration.offset_phi @@ -1073,7 +1083,7 @@

    Methods

    def __init__(self): - self.setup = GlobalState.setup + self.setup = load_setup() self.keep_acquiring_fov_hk = True def run(self, commander, poller): @@ -1244,13 +1254,20 @@

    Methods

    # Retrieve the rotation angles around the y- and z-axis [degrees] # (the rotation angle around the x-axis and the translation are not needed here) + rot_y_z_mini = 1 / 3600 # lower threshold: 1 arcsecond = 1/3600 degree + *_, hexapod_rot_y, hexapod_rot_z = monitoring_info["user"] - hexapod_rot_y = radians(hexapod_rot_y) # Rotation angle around the x-axis [radians] - hexapod_rot_z = radians(hexapod_rot_z) # Rotation angle around the y-axis [radians] + if hexapod_rot_y < rot_y_z_mini and hexapod_rot_z < rot_y_z_mini: + theta = 0 + phi = 0 - theta = degrees(acos(cos(hexapod_rot_y) * cos(hexapod_rot_z))) - phi = degrees(-asin(sin(hexapod_rot_z) / sqrt(1 - pow(cos(hexapod_rot_y) * cos(hexapod_rot_z), 2)))) + else: + hexapod_rot_y = radians(hexapod_rot_y) # Rotation angle around the x-axis [radians] + hexapod_rot_z = radians(hexapod_rot_z) # Rotation angle around the y-axis [radians] + + theta = degrees(acos(cos(hexapod_rot_y) * cos(hexapod_rot_z))) + phi = degrees(atan2(sin(hexapod_rot_z), -sin(hexapod_rot_y) * cos(hexapod_rot_z))) store_actual_fov_position(theta, phi) @@ -1297,13 +1314,20 @@

    Methods

    # Retrieve the rotation angles around the y- and z-axis [degrees] # (the rotation angle around the x-axis and the translation are not needed here) + rot_y_z_mini = 1 / 3600 # lower threshold: 1 arcsecond = 1/3600 degree + *_, hexapod_rot_y, hexapod_rot_z = monitoring_info["user"] - hexapod_rot_y = radians(hexapod_rot_y) # Rotation angle around the x-axis [radians] - hexapod_rot_z = radians(hexapod_rot_z) # Rotation angle around the y-axis [radians] + if hexapod_rot_y < rot_y_z_mini and hexapod_rot_z < rot_y_z_mini: + theta = 0 + phi = 0 - theta = degrees(acos(cos(hexapod_rot_y) * cos(hexapod_rot_z))) - phi = degrees(-asin(sin(hexapod_rot_z) / sqrt(1 - pow(cos(hexapod_rot_y) * cos(hexapod_rot_z), 2)))) + else: + hexapod_rot_y = radians(hexapod_rot_y) # Rotation angle around the x-axis [radians] + hexapod_rot_z = radians(hexapod_rot_z) # Rotation angle around the y-axis [radians] + + theta = degrees(acos(cos(hexapod_rot_y) * cos(hexapod_rot_z))) + phi = degrees(atan2(sin(hexapod_rot_z), -sin(hexapod_rot_y) * cos(hexapod_rot_z))) store_actual_fov_position(theta, phi) @@ -1382,13 +1406,14 @@

    Methods

    # # According to inta_point_source_to_fov: - # grx = theta * np.cos(np.deg2rad(phi)) - # gry = theta * np.sin(np.deg2rad(phi)) + # grx = np.rad2deg(-np.arcsin(np.sin(np.deg2rad(theta)) * np.sin(np.deg2rad(phi)))) + # gry = np.rad2deg(np.arctan2(np.sin(np.deg2rad(theta)) * np.cos(np.deg2rad(phi)), np.cos(np.deg2rad(theta)))) # # grx and gry are already in degrees, therefore: # - theta = sqrt(grx ** 2 + gry ** 2) - phi = degrees(atan2(gry, grx)) + + theta = np.rad2deg(np.arccos(np.cos(np.deg2rad(grx))*np.cos(np.deg2rad(gry)))) + phi = np.rad2deg(np.arctan2(-np.sin(np.deg2rad(grx)), np.cos(np.deg2rad(grx))*np.sin(np.deg2rad(gry)))) store_actual_fov_position(theta, phi) @@ -1441,13 +1466,14 @@

    Methods

    # # According to inta_point_source_to_fov: - # grx = theta * np.cos(np.deg2rad(phi)) - # gry = theta * np.sin(np.deg2rad(phi)) + # grx = np.rad2deg(-np.arcsin(np.sin(np.deg2rad(theta)) * np.sin(np.deg2rad(phi)))) + # gry = np.rad2deg(np.arctan2(np.sin(np.deg2rad(theta)) * np.cos(np.deg2rad(phi)), np.cos(np.deg2rad(theta)))) # # grx and gry are already in degrees, therefore: # - theta = sqrt(grx ** 2 + gry ** 2) - phi = degrees(atan2(gry, grx)) + + theta = np.rad2deg(np.arccos(np.cos(np.deg2rad(grx))*np.cos(np.deg2rad(gry)))) + phi = np.rad2deg(np.arctan2(-np.sin(np.deg2rad(grx)), np.cos(np.deg2rad(grx))*np.sin(np.deg2rad(gry)))) store_actual_fov_position(theta, phi) @@ -1527,27 +1553,29 @@

    Methods

    rotx = radians(monitoring_info["alpha"]) # [radians] roty = radians(monitoring_info["beta"]) # [radians] + if isinstance(roty, float): + rotx = np.array([rotx]) + roty = np.array([roty]) + tolerance = 1.e-5 - # Gnomonic distance to optical axis and in-field angle [degrees] + # Corrections based on #598 + # Copied from camtest.commanding.function.sron_functions.gimbal_rotations_to_fov_angles + theta = np.arccos(np.cos(rotx) * np.cos(roty)) - theta = degrees(acos(cos(rotx) * cos(roty))) # [degrees] + phi = np.zeros_like(roty) - phi = 0.0 - if abs(roty) < tolerance: - phi = sign(rotx) * pi / 2. - elif abs(roty) > tolerance: - phi = atan(sign(roty) * tan(rotx) / sin(roty)) + sel = np.where(np.abs(roty) < tolerance) - phi = degrees(phi) # [degrees] + phi[sel] = np.sign(rotx[sel]) * np.pi/2. - if roty < 0: - phi = -180. - phi + sel = np.where(np.abs(roty) > tolerance) - if phi < -180: - phi += 360. + phi[sel] = np.arctan2(-np.sin(-rotx[sel]) * np.cos(roty[sel]), np.sin(roty[sel])) - store_actual_fov_position(theta, phi) + theta, phi = np.rad2deg(theta), np.rad2deg(phi) + + store_actual_fov_position(theta[0], phi[0]) elif is_control_server_active(self.cmd_endpoint): self.monitoring_timeout += 0.5 @@ -1599,27 +1627,29 @@

    Methods

    rotx = radians(monitoring_info["alpha"]) # [radians] roty = radians(monitoring_info["beta"]) # [radians] + if isinstance(roty, float): + rotx = np.array([rotx]) + roty = np.array([roty]) + tolerance = 1.e-5 - # Gnomonic distance to optical axis and in-field angle [degrees] + # Corrections based on #598 + # Copied from camtest.commanding.function.sron_functions.gimbal_rotations_to_fov_angles + theta = np.arccos(np.cos(rotx) * np.cos(roty)) - theta = degrees(acos(cos(rotx) * cos(roty))) # [degrees] + phi = np.zeros_like(roty) - phi = 0.0 - if abs(roty) < tolerance: - phi = sign(rotx) * pi / 2. - elif abs(roty) > tolerance: - phi = atan(sign(roty) * tan(rotx) / sin(roty)) + sel = np.where(np.abs(roty) < tolerance) - phi = degrees(phi) # [degrees] + phi[sel] = np.sign(rotx[sel]) * np.pi/2. - if roty < 0: - phi = -180. - phi + sel = np.where(np.abs(roty) > tolerance) - if phi < -180: - phi += 360. + phi[sel] = np.arctan2(-np.sin(-rotx[sel]) * np.cos(roty[sel]), np.sin(roty[sel])) - store_actual_fov_position(theta, phi) + theta, phi = np.rad2deg(theta), np.rad2deg(phi) + + store_actual_fov_position(theta[0], phi[0]) elif is_control_server_active(self.cmd_endpoint): self.monitoring_timeout += 0.5 diff --git a/docs/api/egse/fov/fov_ui.html b/docs/api/egse/fov/fov_ui.html index 38dad1f..e9aa9e1 100644 --- a/docs/api/egse/fov/fov_ui.html +++ b/docs/api/egse/fov/fov_ui.html @@ -48,7 +48,7 @@

    Module egse.fov.fov_ui

    from egse.observer import Observer, Observable from egse.resource import get_resource from egse.settings import Settings -from egse.state import GlobalState +from egse.setup import load_setup from egse.zmq_ser import connect_address FONT = QFont("Helvetica", 18) @@ -177,7 +177,7 @@

    Module egse.fov.fov_ui

    # Information from the setup - setup = GlobalState.setup + setup = load_setup() self.focal_length = setup.camera.fov.focal_length_mm self.offset_alpha = setup.gse.stages.calibration.offset_alpha @@ -189,8 +189,6 @@

    Module egse.fov.fov_ui

    self.distortion_coefficients = setup.camera.fov.distortion_coefficients self.height_collimated_beam = setup.gse.stages.calibration.height_collimated_beam - - @pyqtSlot() def run(self): """ Keep on listening on the monitoring port of the stages. @@ -322,7 +320,7 @@

    Module egse.fov.fov_ui

    # CCD coordinates [pixels] - (row, column, ccd_code) = focal_plane_to_ccd_coordinates(x, y) + (row, column, ccd_code) = focal_plane_to_ccd_coordinates(x, y, setup) position = { "angles": (theta, phi), @@ -498,7 +496,7 @@

    Module egse.fov.fov_ui

    self.setGeometry(300, 300, 1500, 1000) self.setWindowTitle("FOV Position GUI") - setup = GlobalState.setup + setup = load_setup() self.fov_radius_mm = setup.camera.fov.radius_mm # Monitoring the stages @@ -1014,7 +1012,7 @@

    Methods

    class FOVUIView
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Open a window and initialiase the GUI. The GUI is divided into two parts. On the @@ -1042,7 +1040,7 @@

    Methods

    self.setGeometry(300, 300, 1500, 1000) self.setWindowTitle("FOV Position GUI") - setup = GlobalState.setup + setup = load_setup() self.fov_radius_mm = setup.camera.fov.radius_mm # Monitoring the stages @@ -2126,7 +2124,7 @@

    Inherited members

    (settings)
    -

    QObject(parent: QObject = None)

    +

    QObject(parent: typing.Optional[QObject] = None)

    Initialisation of a monitoring worker.

    This monitoring worker will listen on the monitoring port of the Control Server, based on the given settings.

    Args

    @@ -2392,7 +2390,7 @@

    Args

    # Information from the setup - setup = GlobalState.setup + setup = load_setup() self.focal_length = setup.camera.fov.focal_length_mm self.offset_alpha = setup.gse.stages.calibration.offset_alpha @@ -2404,8 +2402,6 @@

    Args

    self.distortion_coefficients = setup.camera.fov.distortion_coefficients self.height_collimated_beam = setup.gse.stages.calibration.height_collimated_beam - - @pyqtSlot() def run(self): """ Keep on listening on the monitoring port of the stages. @@ -2537,7 +2533,7 @@

    Args

    # CCD coordinates [pixels] - (row, column, ccd_code) = focal_plane_to_ccd_coordinates(x, y) + (row, column, ccd_code) = focal_plane_to_ccd_coordinates(x, y, setup) position = { "angles": (theta, phi), @@ -2650,7 +2646,7 @@

    Methods

    # CCD coordinates [pixels] - (row, column, ccd_code) = focal_plane_to_ccd_coordinates(x, y) + (row, column, ccd_code) = focal_plane_to_ccd_coordinates(x, y, setup) position = { "angles": (theta, phi), diff --git a/docs/api/egse/fov/fov_ui_model.html b/docs/api/egse/fov/fov_ui_model.html index 50c1a28..b1ccd8d 100644 --- a/docs/api/egse/fov/fov_ui_model.html +++ b/docs/api/egse/fov/fov_ui_model.html @@ -41,7 +41,7 @@

    Module egse.fov.fov_ui_model

    from egse.coordinates import undistorted_to_distorted_focal_plane_coordinates from egse.hexapod.symetrie.puna_ui import PunaUIModel from egse.settings import Settings -from egse.setup import get_setup +from egse.setup import load_setup from egse.stages.huber.smc9300_ui import HuberUIModel STAGES_SETTINGS = Settings.load("Huber Controller") @@ -64,6 +64,7 @@

    Module egse.fov.fov_ui_model

    self.hexapod_model = PunaUIModel(type_hexapod) self.stages_model = HuberUIModel(type_stages) + self.setup = load_setup() ############## # Connectivity @@ -111,9 +112,9 @@

    Module egse.fov.fov_ui_model

    - "mm": focal-plane coordinates (x, y) [mm] """ - setup = get_setup() - offset_phi = setup.gse.stages.calibration.offset_phi - offset_alpha = setup.gse.stages.calibration.offset_alpha + offset_phi = self.setup.gse.stages.calibration.offset_phi + offset_alpha = self.setup.gse.stages.calibration.offset_alpha + alpha_correction_coefficients = self.setup.gse.stages.calibration.alpha_correction_coefficients # Focal-plane coordinates [mm] @@ -121,12 +122,13 @@

    Module egse.fov.fov_ui_model

    # CCD coordinates [pixels] - (row, column, ccd_code) = focal_plane_to_ccd_coordinates(x, y) + (row, column, ccd_code) = focal_plane_to_ccd_coordinates(x, y, setup) # Gnomonic distance to optical axis and in-field angle [degrees] angle_small_rotation_stage = self.get_rotation_angle_small_rotation_stage() - theta = 2 * (angle_small_rotation_stage + offset_alpha) + theta = (angle_small_rotation_stage + offset_alpha - alpha_correction_coefficients[0]) \ + / alpha_correction_coefficients[1] position = { "angles": (theta, offset_phi - self.get_rotation_angle_big_rotation_stage()), @@ -266,6 +268,7 @@

    Classes

    self.hexapod_model = PunaUIModel(type_hexapod) self.stages_model = HuberUIModel(type_stages) + self.setup = load_setup() ############## # Connectivity @@ -313,9 +316,9 @@

    Classes

    - "mm": focal-plane coordinates (x, y) [mm] """ - setup = get_setup() - offset_phi = setup.gse.stages.calibration.offset_phi - offset_alpha = setup.gse.stages.calibration.offset_alpha + offset_phi = self.setup.gse.stages.calibration.offset_phi + offset_alpha = self.setup.gse.stages.calibration.offset_alpha + alpha_correction_coefficients = self.setup.gse.stages.calibration.alpha_correction_coefficients # Focal-plane coordinates [mm] @@ -323,12 +326,13 @@

    Classes

    # CCD coordinates [pixels] - (row, column, ccd_code) = focal_plane_to_ccd_coordinates(x, y) + (row, column, ccd_code) = focal_plane_to_ccd_coordinates(x, y, setup) # Gnomonic distance to optical axis and in-field angle [degrees] angle_small_rotation_stage = self.get_rotation_angle_small_rotation_stage() - theta = 2 * (angle_small_rotation_stage + offset_alpha) + theta = (angle_small_rotation_stage + offset_alpha - alpha_correction_coefficients[0]) \ + / alpha_correction_coefficients[1] position = { "angles": (theta, offset_phi - self.get_rotation_angle_big_rotation_stage()), @@ -565,9 +569,9 @@

    Methods

    - "mm": focal-plane coordinates (x, y) [mm] """ - setup = get_setup() - offset_phi = setup.gse.stages.calibration.offset_phi - offset_alpha = setup.gse.stages.calibration.offset_alpha + offset_phi = self.setup.gse.stages.calibration.offset_phi + offset_alpha = self.setup.gse.stages.calibration.offset_alpha + alpha_correction_coefficients = self.setup.gse.stages.calibration.alpha_correction_coefficients # Focal-plane coordinates [mm] @@ -575,12 +579,13 @@

    Methods

    # CCD coordinates [pixels] - (row, column, ccd_code) = focal_plane_to_ccd_coordinates(x, y) + (row, column, ccd_code) = focal_plane_to_ccd_coordinates(x, y, setup) # Gnomonic distance to optical axis and in-field angle [degrees] angle_small_rotation_stage = self.get_rotation_angle_small_rotation_stage() - theta = 2 * (angle_small_rotation_stage + offset_alpha) + theta = (angle_small_rotation_stage + offset_alpha - alpha_correction_coefficients[0]) \ + / alpha_correction_coefficients[1] position = { "angles": (theta, offset_phi - self.get_rotation_angle_big_rotation_stage()), diff --git a/docs/api/egse/fov/fov_ui_view.html b/docs/api/egse/fov/fov_ui_view.html index 8e6054d..e51eaf2 100644 --- a/docs/api/egse/fov/fov_ui_view.html +++ b/docs/api/egse/fov/fov_ui_view.html @@ -387,7 +387,7 @@

    Classes

    class FOVUIView
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Open a window and initialiase the GUI. The GUI is divided into two parts. On the diff --git a/docs/api/egse/gimbal/symetrie/generic_gimbal_ui.html b/docs/api/egse/gimbal/symetrie/generic_gimbal_ui.html index 12f95e5..eaa83df 100644 --- a/docs/api/egse/gimbal/symetrie/generic_gimbal_ui.html +++ b/docs/api/egse/gimbal/symetrie/generic_gimbal_ui.html @@ -3235,7 +3235,7 @@

    Methods

    class GimbalUIView
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -5160,7 +5160,7 @@

    Methods

    (view, observable)
    -

    QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -5464,7 +5464,7 @@

    Methods

    def eventFilter(self, source, event)
    -

    eventFilter(self, QObject, QEvent) -> bool

    +

    eventFilter(self, a0: typing.Optional[QObject], a1: typing.Optional[QEvent]) -> bool

    Expand source code @@ -5492,7 +5492,7 @@

    Methods

    def setToolTip(self, message: str) ‑> None
    -

    setToolTip(self, str)

    +

    setToolTip(self, a0: str)

    Expand source code diff --git a/docs/api/egse/gimbal/symetrie/gimbal_cs.html b/docs/api/egse/gimbal/symetrie/gimbal_cs.html index cc9a212..9e03c8b 100644 --- a/docs/api/egse/gimbal/symetrie/gimbal_cs.html +++ b/docs/api/egse/gimbal/symetrie/gimbal_cs.html @@ -129,7 +129,7 @@

    Module egse.gimbal.symetrie.gimbal_cs

    def before_serve(self): start_http_server(CTRL_SETTINGS.METRICS_PORT) - + @click.group() def cli(): @@ -396,10 +396,15 @@

    Inherited members

    • ControlServer:
    • diff --git a/docs/api/egse/gimbal/symetrie/gimbal_protocol.html b/docs/api/egse/gimbal/symetrie/gimbal_protocol.html index f685bb3..fd8ace4 100644 --- a/docs/api/egse/gimbal/symetrie/gimbal_protocol.html +++ b/docs/api/egse/gimbal/symetrie/gimbal_protocol.html @@ -37,8 +37,10 @@

      Module egse.gimbal.symetrie.gimbal_protocol

      from egse.hk import read_conversion_dict, convert_hk_names from egse.protocol import CommandProtocol from egse.settings import Settings +from egse.setup import load_setup from egse.system import format_datetime from egse.zmq_ser import bind_address +from egse.metrics import define_metrics logger = logging.getLogger(__name__) @@ -54,8 +56,10 @@

      Module egse.gimbal.symetrie.gimbal_protocol

      def __init__(self, control_server: ControlServer): super().__init__() self.control_server = control_server + setup = load_setup() - self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), use_site=True) + self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), use_site=True, + setup=setup) if Settings.simulation_mode(): self.gimbal = GimbalSimulator() @@ -72,6 +76,8 @@

      Module egse.gimbal.symetrie.gimbal_protocol

      self.load_commands(gimbal_settings.Commands, GimbalCommand, GimbalInterface) self.build_device_method_lookup_table(self.gimbal) + self.metrics = define_metrics(origin="GIMBAL", use_site=True, setup=setup) + def get_bind_address(self): return bind_address( self.control_server.get_communication_protocol(), @@ -95,6 +101,8 @@

      Module egse.gimbal.symetrie.gimbal_protocol

      def get_housekeeping(self) -> dict: + metrics_dict = self.metrics + result = dict() result["timestamp"] = format_datetime() @@ -138,9 +146,16 @@

      Module egse.gimbal.symetrie.gimbal_protocol

      result["Homing done"] = self.gimbal.is_homing_done() result["In position"] = self.gimbal.is_in_position() - - return convert_hk_names(result, self.hk_conversion_table) - + temperatures = self.gimbal.get_motor_temperatures() + result["Temp_X"] = temperatures[0] + result["Temp_Y"] = temperatures[1] + hk_dict = convert_hk_names(result, self.hk_conversion_table) + + for hk_name in metrics_dict.keys(): + metrics_dict[hk_name].set(hk_dict[hk_name]) + + return hk_dict + def is_device_connected(self): # FIXME(rik): There must be another way to check if the socket is still alive... # This will send way too many VERSION requests to the controllers. @@ -214,8 +229,10 @@

      Inherited members

      def __init__(self, control_server: ControlServer): super().__init__() self.control_server = control_server + setup = load_setup() - self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), use_site=True) + self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), use_site=True, + setup=setup) if Settings.simulation_mode(): self.gimbal = GimbalSimulator() @@ -232,6 +249,8 @@

      Inherited members

      self.load_commands(gimbal_settings.Commands, GimbalCommand, GimbalInterface) self.build_device_method_lookup_table(self.gimbal) + self.metrics = define_metrics(origin="GIMBAL", use_site=True, setup=setup) + def get_bind_address(self): return bind_address( self.control_server.get_communication_protocol(), @@ -255,6 +274,8 @@

      Inherited members

      def get_housekeeping(self) -> dict: + metrics_dict = self.metrics + result = dict() result["timestamp"] = format_datetime() @@ -298,9 +319,16 @@

      Inherited members

      result["Homing done"] = self.gimbal.is_homing_done() result["In position"] = self.gimbal.is_in_position() - - return convert_hk_names(result, self.hk_conversion_table) - + temperatures = self.gimbal.get_motor_temperatures() + result["Temp_X"] = temperatures[0] + result["Temp_Y"] = temperatures[1] + hk_dict = convert_hk_names(result, self.hk_conversion_table) + + for hk_name in metrics_dict.keys(): + metrics_dict[hk_name].set(hk_dict[hk_name]) + + return hk_dict + def is_device_connected(self): # FIXME(rik): There must be another way to check if the socket is still alive... # This will send way too many VERSION requests to the controllers. diff --git a/docs/api/egse/gimbal/symetrie/gimbal_ui.html b/docs/api/egse/gimbal/symetrie/gimbal_ui.html index 957689c..2dda0c8 100644 --- a/docs/api/egse/gimbal/symetrie/gimbal_ui.html +++ b/docs/api/egse/gimbal/symetrie/gimbal_ui.html @@ -607,7 +607,7 @@

      Methods

      class GimbalUIView
      -

      QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      +

      QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      Expand source code diff --git a/docs/api/egse/gui/buttons.html b/docs/api/egse/gui/buttons.html index 2de4054..149d986 100644 --- a/docs/api/egse/gui/buttons.html +++ b/docs/api/egse/gui/buttons.html @@ -420,8 +420,8 @@

      Classes

      (width: int = 32, height: int = 32, name: str = None, status_tip: str = None, selected: Union[str, pathlib.Path] = None, not_selected: Union[str, pathlib.Path] = None, no_change: Union[str, pathlib.Path] = None, disabled: Union[str, pathlib.Path, list] = None, tristate: bool = False, parent: Optional[PyQt5.QtWidgets.QWidget] = None)
      -

      QCheckBox(parent: QWidget = None) -QCheckBox(str, parent: QWidget = None)

      +

      QCheckBox(parent: typing.Optional[QWidget] = None) +QCheckBox(text: str, parent: typing.Optional[QWidget] = None)

      Expand source code @@ -594,7 +594,7 @@

      Methods

      def hitButton(self, pos: PyQt5.QtCore.QPoint)
      -

      hitButton(self, QPoint) -> bool

      +

      hitButton(self, pos: QPoint) -> bool

      Expand source code @@ -620,7 +620,7 @@

      Methods

      def paintEvent(self, *args, **kwargs)
      -

      paintEvent(self, QPaintEvent)

      +

      paintEvent(self, a0: typing.Optional[QPaintEvent])

      Expand source code @@ -639,7 +639,7 @@

      Methods

      def setDisabled(self, flag: bool = True)
      -

      setDisabled(self, bool)

      +

      setDisabled(self, a0: bool)

      Expand source code @@ -671,7 +671,7 @@

      Methods

      (width: int = 32, height: int = 32, name: str = None, status_tip: str = None, selected: Union[str, pathlib.Path] = None, disabled: Union[str, pathlib.Path] = None, parent: Optional[PyQt5.QtWidgets.QWidget] = None)
      -

      QToolButton(parent: QWidget = None)

      +

      QToolButton(parent: typing.Optional[QWidget] = None)

      Expand source code @@ -824,7 +824,7 @@

      Methods

      def hitButton(self, pos: PyQt5.QtCore.QPoint)
      -

      hitButton(self, QPoint) -> bool

      +

      hitButton(self, pos: QPoint) -> bool

      Expand source code @@ -837,7 +837,7 @@

      Methods

      def paintEvent(self, *args, **kwargs)
      -

      paintEvent(self, QPaintEvent)

      +

      paintEvent(self, a0: typing.Optional[QPaintEvent])

      Expand source code @@ -898,7 +898,7 @@

      Methods

      def setDisabled(self, flag: bool = True)
      -

      setDisabled(self, bool)

      +

      setDisabled(self, a0: bool)

      Expand source code @@ -916,9 +916,9 @@

      Methods

      (width: int = 32, height: int = 32, name: str = None, status_tip: str = None, selected: Union[str, pathlib.Path] = None, disabled: Union[str, pathlib.Path] = None, parent: Optional[PyQt5.QtWidgets.QWidget] = None)
      -

      QPushButton(parent: QWidget = None) -QPushButton(str, parent: QWidget = None) -QPushButton(QIcon, str, parent: QWidget = None)

      +

      QPushButton(parent: typing.Optional[QWidget] = None) +QPushButton(text: str, parent: typing.Optional[QWidget] = None) +QPushButton(icon: QIcon, text: str, parent: typing.Optional[QWidget] = None)

      Expand source code @@ -1071,7 +1071,7 @@

      Methods

      def hitButton(self, pos: PyQt5.QtCore.QPoint)
      -

      hitButton(self, QPoint) -> bool

      +

      hitButton(self, pos: QPoint) -> bool

      Expand source code @@ -1084,7 +1084,7 @@

      Methods

      def paintEvent(self, *args, **kwargs)
      -

      paintEvent(self, QPaintEvent)

      +

      paintEvent(self, a0: typing.Optional[QPaintEvent])

      Expand source code @@ -1145,7 +1145,7 @@

      Methods

      def setDisabled(self, flag: bool = True)
      -

      setDisabled(self, bool)

      +

      setDisabled(self, a0: bool)

      Expand source code diff --git a/docs/api/egse/gui/focalplane.html b/docs/api/egse/gui/focalplane.html index 36eba34..953270d 100644 --- a/docs/api/egse/gui/focalplane.html +++ b/docs/api/egse/gui/focalplane.html @@ -61,7 +61,7 @@

      Module egse.gui.focalplane

      from egse.coordinates import focal_plane_coordinates_to_angles from egse.gui.formatter import degree_formatter from egse.settings import Settings -from egse.state import GlobalState +from egse.setup import load_setup FONT = QFont("Helvetica", 18) @@ -96,8 +96,11 @@

      Module egse.gui.focalplane

      Initialisation of a plot of the focal plane, with a blue circle indicating the field-of-view. """ - self.fee_side = GlobalState.setup.camera.fee.ccd_sides.enum - self.ccd_id = GlobalState.setup.camera.fee.ccd_numbering.CCD_ID + + setup = load_setup() + + self.fee_side = setup.camera.fee.ccd_sides.enum + self.ccd_id = setup.camera.fee.ccd_numbering.CCD_ID self.figure = Figure() self.ax = self.figure.add_subplot(111) @@ -647,7 +650,7 @@

      Module egse.gui.focalplane

      if self.subfield_is_drawn: - del self.ax.lines[-1] + self.ax.lines[-1].remove() self.subfield_is_drawn = False if self.ccd_code is None: @@ -806,7 +809,7 @@

      Module egse.gui.focalplane

      if self.source_position_is_drawn: - del self.ax.lines[-1] + self.ax.lines[-1].remove() self.source_position_is_drawn = False if (x_fp is None) or (y_fp is None): @@ -995,7 +998,8 @@

      Module egse.gui.focalplane

      num_points = len(self.visited_x_fp) while num_points > 0: - del self.ax.lines[-1] + # del self.ax.lines[-1] # TODO + self.ax.lines[-1].remove() num_points -= 1 self.visited_x_fp = np.array([]) @@ -1358,8 +1362,11 @@

      Classes

      Initialisation of a plot of the focal plane, with a blue circle indicating the field-of-view. """ - self.fee_side = GlobalState.setup.camera.fee.ccd_sides.enum - self.ccd_id = GlobalState.setup.camera.fee.ccd_numbering.CCD_ID + + setup = load_setup() + + self.fee_side = setup.camera.fee.ccd_sides.enum + self.ccd_id = setup.camera.fee.ccd_numbering.CCD_ID self.figure = Figure() self.ax = self.figure.add_subplot(111) @@ -1782,12 +1789,12 @@

      Ancestors

    • matplotlib.backends.backend_qtagg.FigureCanvasQTAgg
    • matplotlib.backends.backend_agg.FigureCanvasAgg
    • matplotlib.backends.backend_qt.FigureCanvasQT
    • +
    • matplotlib.backend_bases.FigureCanvasBase
    • PyQt5.QtWidgets.QWidget
    • PyQt5.QtCore.QObject
    • sip.wrapper
    • PyQt5.QtGui.QPaintDevice
    • sip.simplewrapper
    • -
    • matplotlib.backend_bases.FigureCanvasBase

    Subclasses

      @@ -3118,7 +3125,7 @@

      Inherited members

      if self.source_position_is_drawn: - del self.ax.lines[-1] + self.ax.lines[-1].remove() self.source_position_is_drawn = False if (x_fp is None) or (y_fp is None): @@ -3158,12 +3165,12 @@

      Ancestors

    • matplotlib.backends.backend_qtagg.FigureCanvasQTAgg
    • matplotlib.backends.backend_agg.FigureCanvasAgg
    • matplotlib.backends.backend_qt.FigureCanvasQT
    • +
    • matplotlib.backend_bases.FigureCanvasBase
    • PyQt5.QtWidgets.QWidget
    • PyQt5.QtCore.QObject
    • sip.wrapper
    • PyQt5.QtGui.QPaintDevice
    • sip.simplewrapper
    • -
    • matplotlib.backend_bases.FigureCanvasBase

    Methods

    @@ -3308,7 +3315,7 @@

    Methods

    if self.source_position_is_drawn: - del self.ax.lines[-1] + self.ax.lines[-1].remove() self.source_position_is_drawn = False if (x_fp is None) or (y_fp is None): @@ -3492,7 +3499,7 @@

    Methods

    if self.subfield_is_drawn: - del self.ax.lines[-1] + self.ax.lines[-1].remove() self.subfield_is_drawn = False if self.ccd_code is None: @@ -3551,12 +3558,12 @@

    Ancestors

  • matplotlib.backends.backend_qtagg.FigureCanvasQTAgg
  • matplotlib.backends.backend_agg.FigureCanvasAgg
  • matplotlib.backends.backend_qt.FigureCanvasQT
  • +
  • matplotlib.backend_bases.FigureCanvasBase
  • PyQt5.QtWidgets.QWidget
  • PyQt5.QtCore.QObject
  • sip.wrapper
  • PyQt5.QtGui.QPaintDevice
  • sip.simplewrapper
  • -
  • matplotlib.backend_bases.FigureCanvasBase
  • Methods

    @@ -3740,7 +3747,7 @@

    Methods

    if self.subfield_is_drawn: - del self.ax.lines[-1] + self.ax.lines[-1].remove() self.subfield_is_drawn = False if self.ccd_code is None: @@ -3966,7 +3973,8 @@

    Methods

    num_points = len(self.visited_x_fp) while num_points > 0: - del self.ax.lines[-1] + # del self.ax.lines[-1] # TODO + self.ax.lines[-1].remove() num_points -= 1 self.visited_x_fp = np.array([]) @@ -3980,12 +3988,12 @@

    Ancestors

  • matplotlib.backends.backend_qtagg.FigureCanvasQTAgg
  • matplotlib.backends.backend_agg.FigureCanvasAgg
  • matplotlib.backends.backend_qt.FigureCanvasQT
  • +
  • matplotlib.backend_bases.FigureCanvasBase
  • PyQt5.QtWidgets.QWidget
  • PyQt5.QtCore.QObject
  • sip.wrapper
  • PyQt5.QtGui.QPaintDevice
  • sip.simplewrapper
  • -
  • matplotlib.backend_bases.FigureCanvasBase
  • Methods

    @@ -4120,7 +4128,8 @@

    Methods

    num_points = len(self.visited_x_fp) while num_points > 0: - del self.ax.lines[-1] + # del self.ax.lines[-1] # TODO + self.ax.lines[-1].remove() num_points -= 1 self.visited_x_fp = np.array([]) diff --git a/docs/api/egse/gui/mechanisms.html b/docs/api/egse/gui/mechanisms.html index 52dcb82..db94ba8 100644 --- a/docs/api/egse/gui/mechanisms.html +++ b/docs/api/egse/gui/mechanisms.html @@ -41,8 +41,7 @@

    Module egse.gui.mechanisms

    from egse.config import find_file from egse.resource import get_resource from egse.settings import Settings -from egse.setup import get_setup -from egse.state import GlobalState +from egse.setup import load_setup FONT = QFont("Helvetica", 18) @@ -118,7 +117,7 @@

    Module egse.gui.mechanisms

    FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding) FigureCanvas.updateGeometry(self) - setup = GlobalState.setup + setup = load_setup() self.offset_alpha = setup.gse.stages.calibration.offset_alpha self.offset_delta_x = setup.gse.stages.calibration.offset_delta_x @@ -209,7 +208,7 @@

    Module egse.gui.mechanisms

    self.arrow_right.remove() self.arrow_left.remove() - del self.ax.lines[-1] + self.ax.lines[-1].remove() # Scan mirror @@ -259,7 +258,7 @@

    Module egse.gui.mechanisms

    "Current configuration of the scan mirror assembly (i.e. small rotation stage and translation stage)") self.setFont(FONT) - setup = get_setup() + setup = load_setup() self.offset_delta_x = setup.gse.stages.calibration.offset_delta_x self.offset_alpha = setup.gse.stages.calibration.offset_alpha @@ -314,7 +313,7 @@

    Module egse.gui.mechanisms

    self.sma.update_plot(-distance, angle) self.cmd_angle_text.setText(f"Commanded position for the small rotation stage: {angle:.2f}{DEGREE_SYMBOL_LABEL}") - self.cmd_distance_text.setText(f"Commanded position for the translation stage: {distance:.2f}{DEGREE_SYMBOL_LABEL}") + self.cmd_distance_text.setText(f"Commanded position for the translation stage: {distance:.2f}mm") self.cmd_angle_text_explanation.setText(f"Commanded position for the small rotation stage: {angle:.2f}{DEGREE_SYMBOL_LABEL}") self.total_angle_text_explanation.setText(f"Angle \u03B1 shown: {(angle + self.offset_alpha + 45):.2f}{DEGREE_SYMBOL_LABEL}") @@ -421,7 +420,7 @@

    Module egse.gui.mechanisms

    "Current configuration of the big rotation stage (visualised as the rotation of GL_ROT w.r.t. GL_FIX)") self.setFont(FONT) - setup = get_setup() + setup = load_setup() self.offset_phi = setup.gse.stages.calibration.offset_phi self.explanation_window = ExplanationWindow("Configuration of the big rotation stage", @@ -719,12 +718,12 @@

    Ancestors

  • matplotlib.backends.backend_qtagg.FigureCanvasQTAgg
  • matplotlib.backends.backend_agg.FigureCanvasAgg
  • matplotlib.backends.backend_qt.FigureCanvasQT
  • +
  • matplotlib.backend_bases.FigureCanvasBase
  • PyQt5.QtWidgets.QWidget
  • PyQt5.QtCore.QObject
  • sip.wrapper
  • PyQt5.QtGui.QPaintDevice
  • sip.simplewrapper
  • -
  • matplotlib.backend_bases.FigureCanvasBase
  • Methods

    @@ -815,8 +814,8 @@

    Methods

    class BigRotationStageWidget
    -

    QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

    +

    QGroupBox(parent: typing.Optional[QWidget] = None) +QGroupBox(title: str, parent: typing.Optional[QWidget] = None)

    Initialisation of a plot showing the orientation of GL_ROT w.r.t. GL_FIX, based on the orientation of the big rotation stage. Below the @@ -840,7 +839,7 @@

    Methods

    "Current configuration of the big rotation stage (visualised as the rotation of GL_ROT w.r.t. GL_FIX)") self.setFont(FONT) - setup = get_setup() + setup = load_setup() self.offset_phi = setup.gse.stages.calibration.offset_phi self.explanation_window = ExplanationWindow("Configuration of the big rotation stage", @@ -951,7 +950,7 @@

    Methods

    (title, filename)
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Initialisation of a window with more explanation about the mechanisms.

    Args

      @@ -1129,12 +1128,12 @@

      Ancestors

    • matplotlib.backends.backend_qtagg.FigureCanvasQTAgg
    • matplotlib.backends.backend_agg.FigureCanvasAgg
    • matplotlib.backends.backend_qt.FigureCanvasQT
    • +
    • matplotlib.backend_bases.FigureCanvasBase
    • PyQt5.QtWidgets.QWidget
    • PyQt5.QtCore.QObject
    • sip.wrapper
    • PyQt5.QtGui.QPaintDevice
    • sip.simplewrapper
    • -
    • matplotlib.backend_bases.FigureCanvasBase

    Methods

    @@ -1258,8 +1257,8 @@

    Methods

    class FocusPositionWidget
    -

    QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

    +

    QGroupBox(parent: typing.Optional[QWidget] = None) +QGroupBox(title: str, parent: typing.Optional[QWidget] = None)

    Initialisation of a plot showing L6 and FPA_SEN.

    @@ -1387,7 +1386,7 @@

    Ancestors

    FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding) FigureCanvas.updateGeometry(self) - setup = GlobalState.setup + setup = load_setup() self.offset_alpha = setup.gse.stages.calibration.offset_alpha self.offset_delta_x = setup.gse.stages.calibration.offset_delta_x @@ -1478,7 +1477,7 @@

    Ancestors

    self.arrow_right.remove() self.arrow_left.remove() - del self.ax.lines[-1] + self.ax.lines[-1].remove() # Scan mirror @@ -1518,12 +1517,12 @@

    Ancestors

  • matplotlib.backends.backend_qtagg.FigureCanvasQTAgg
  • matplotlib.backends.backend_agg.FigureCanvasAgg
  • matplotlib.backends.backend_qt.FigureCanvasQT
  • +
  • matplotlib.backend_bases.FigureCanvasBase
  • PyQt5.QtWidgets.QWidget
  • PyQt5.QtCore.QObject
  • sip.wrapper
  • PyQt5.QtGui.QPaintDevice
  • sip.simplewrapper
  • -
  • matplotlib.backend_bases.FigureCanvasBase
  • Methods

    @@ -1640,7 +1639,7 @@

    Args

    self.arrow_right.remove() self.arrow_left.remove() - del self.ax.lines[-1] + self.ax.lines[-1].remove() # Scan mirror @@ -1682,8 +1681,8 @@

    Args

    class ScanMirrorAssemblyWidget
    -

    QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

    +

    QGroupBox(parent: typing.Optional[QWidget] = None) +QGroupBox(title: str, parent: typing.Optional[QWidget] = None)

    Initialisation of a plot showing the distance of the scan mirror (along the translation stage) from the optical axis and the orientation of the scan mirror (small rotation stage).

    @@ -1704,7 +1703,7 @@

    Args

    "Current configuration of the scan mirror assembly (i.e. small rotation stage and translation stage)") self.setFont(FONT) - setup = get_setup() + setup = load_setup() self.offset_delta_x = setup.gse.stages.calibration.offset_delta_x self.offset_alpha = setup.gse.stages.calibration.offset_alpha @@ -1759,7 +1758,7 @@

    Args

    self.sma.update_plot(-distance, angle) self.cmd_angle_text.setText(f"Commanded position for the small rotation stage: {angle:.2f}{DEGREE_SYMBOL_LABEL}") - self.cmd_distance_text.setText(f"Commanded position for the translation stage: {distance:.2f}{DEGREE_SYMBOL_LABEL}") + self.cmd_distance_text.setText(f"Commanded position for the translation stage: {distance:.2f}mm") self.cmd_angle_text_explanation.setText(f"Commanded position for the small rotation stage: {angle:.2f}{DEGREE_SYMBOL_LABEL}") self.total_angle_text_explanation.setText(f"Angle \u03B1 shown: {(angle + self.offset_alpha + 45):.2f}{DEGREE_SYMBOL_LABEL}") @@ -1804,7 +1803,7 @@

    Methods

    self.sma.update_plot(-distance, angle) self.cmd_angle_text.setText(f"Commanded position for the small rotation stage: {angle:.2f}{DEGREE_SYMBOL_LABEL}") - self.cmd_distance_text.setText(f"Commanded position for the translation stage: {distance:.2f}{DEGREE_SYMBOL_LABEL}") + self.cmd_distance_text.setText(f"Commanded position for the translation stage: {distance:.2f}mm") self.cmd_angle_text_explanation.setText(f"Commanded position for the small rotation stage: {angle:.2f}{DEGREE_SYMBOL_LABEL}") self.total_angle_text_explanation.setText(f"Angle \u03B1 shown: {(angle + self.offset_alpha + 45):.2f}{DEGREE_SYMBOL_LABEL}") diff --git a/docs/api/egse/gui/states.html b/docs/api/egse/gui/states.html index 884cce5..ae57758 100644 --- a/docs/api/egse/gui/states.html +++ b/docs/api/egse/gui/states.html @@ -190,8 +190,8 @@

    Classes

    (states: List[List[~T]], title: Optional[str] = 'States', shape: ShapeEnum = ShapeEnum.CIRCLE, parent: PyQt5.QtWidgets.QWidget = None)
    -

    QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

    +

    QGroupBox(parent: typing.Optional[QWidget] = None) +QGroupBox(title: str, parent: typing.Optional[QWidget] = None)

    Args

    states : List[List]
    diff --git a/docs/api/egse/gui/stripchart.html b/docs/api/egse/gui/stripchart.html index cad4299..e4f79a0 100644 --- a/docs/api/egse/gui/stripchart.html +++ b/docs/api/egse/gui/stripchart.html @@ -1060,9 +1060,9 @@

    Methods

    update(self) -update(self, QRect) -update(self, QRegion) -update(self, int, int, int, int)

    +update(self, a0: QRect) +update(self, a0: QRegion) +update(self, ax: int, ay: int, aw: int, ah: int)

    Expand source code @@ -1800,8 +1800,8 @@

    Args

    (quantity, unit)
    -

    QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

    +

    QGroupBox(parent: typing.Optional[QWidget] = None) +QGroupBox(title: str, parent: typing.Optional[QWidget] = None)

    Initialisation of a plot of the focal plane, with a blue circle indicating the field-of-view. Below the plot you can find a spinner to choose the coordinate @@ -2156,9 +2156,11 @@

    Args

    showValues (bool) Whether to display values adjacent to ticks pen -(QPen) Pen used when drawing ticks. +(QPen) Pen used when drawing axis and (by default) ticks textPen (QPen) Pen used when drawing tick labels. +tickPen +(QPen) Pen used when drawing ticks. text The text (excluding units) to display on the label for this axis. @@ -2226,7 +2228,12 @@

    Methods

    (quantity, unit, parent=None, dpi=100)
    -

    QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    The canvas the figure renders into.

    +

    Attributes

    +
    +
    figure : ~matplotlib.figure.Figure
    +
    A high-level figure instance.
    +

    Initialisation of the plot window, that will be used to plot time series.

    Args

      @@ -2314,12 +2321,12 @@

      Ancestors

    • matplotlib.backends.backend_qtagg.FigureCanvasQTAgg
    • matplotlib.backends.backend_agg.FigureCanvasAgg
    • matplotlib.backends.backend_qt.FigureCanvasQT
    • +
    • matplotlib.backend_bases.FigureCanvasBase
    • PyQt5.QtWidgets.QWidget
    • PyQt5.QtCore.QObject
    • sip.wrapper
    • PyQt5.QtGui.QPaintDevice
    • sip.simplewrapper
    • -
    • matplotlib.backend_bases.FigureCanvasBase

    Methods

    diff --git a/docs/api/egse/gui/switch.html b/docs/api/egse/gui/switch.html index 8f21475..c615e6d 100644 --- a/docs/api/egse/gui/switch.html +++ b/docs/api/egse/gui/switch.html @@ -180,7 +180,7 @@

    Class variables

    (parent)
    -

    QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -253,7 +253,7 @@

    Methods

    def paintEvent(self, event)
    -

    paintEvent(self, QPaintEvent)

    +

    paintEvent(self, a0: typing.Optional[QPaintEvent])

    Expand source code diff --git a/docs/api/egse/hexapod/symetrie/hexapod_ui.html b/docs/api/egse/hexapod/symetrie/hexapod_ui.html index 70c3413..3cd5346 100644 --- a/docs/api/egse/hexapod/symetrie/hexapod_ui.html +++ b/docs/api/egse/hexapod/symetrie/hexapod_ui.html @@ -3476,7 +3476,7 @@

    Methods

    class HexapodUIView
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -5123,7 +5123,7 @@

    Methods

    (view, observable)
    -

    QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -5433,7 +5433,7 @@

    Methods

    def eventFilter(self, source, event)
    -

    eventFilter(self, QObject, QEvent) -> bool

    +

    eventFilter(self, a0: typing.Optional[QObject], a1: typing.Optional[QEvent]) -> bool

    Expand source code @@ -5461,7 +5461,7 @@

    Methods

    def setToolTip(self, message: str) ‑> None
    -

    setToolTip(self, str)

    +

    setToolTip(self, a0: str)

    Expand source code diff --git a/docs/api/egse/hexapod/symetrie/index.html b/docs/api/egse/hexapod/symetrie/index.html index 2bb30e6..752f969 100644 --- a/docs/api/egse/hexapod/symetrie/index.html +++ b/docs/api/egse/hexapod/symetrie/index.html @@ -99,18 +99,22 @@

    Module egse.hexapod.symetrie

    Note the returned values are for the device hardware controller, not the control server. - If no setup argument is provided, the Setup will be loaded from the GlobalState. + If no setup argument is provided, the Setup will be loaded from the Configuration Manager. """ - from egse.state import GlobalState - from egse.setup import SetupError + from egse.setup import SetupError, load_setup - setup = setup or GlobalState.setup + setup = setup or load_setup() try: - hexapod_id = setup.gse.hexapod.device_args.device_id + try: + hexapod_id = setup.gse.hexapod.device_args.device_id + hexapod_name: str = setup.gse.hexapod.device_args.device_name + except AttributeError: + # Handle older Setups where no device_args existed for the PUNA hexapod + hexapod_id = setup.gse.hexapod.ID + hexapod_name: str = setup.gse.hexapod.device_name hexapod_id = f"H_{hexapod_id}" - hexapod_name: str = setup.gse.hexapod.device_args.device_name hostname: str = PUNA_SETTINGS[hexapod_id]["HOSTNAME"] port: int = int(PUNA_SETTINGS[hexapod_id]["PORT"]) controller_type: str = PUNA_SETTINGS[hexapod_id]["TYPE"] @@ -266,7 +270,7 @@

    Functions

    Returns the hostname (str), port number (int), hexapod id (str), hexapod name (str), and type (str) for the hexapod controller as defined in the Setup and Settings.

    Note the returned values are for the device hardware controller, not the control server.

    -

    If no setup argument is provided, the Setup will be loaded from the GlobalState.

    +

    If no setup argument is provided, the Setup will be loaded from the Configuration Manager.

    Expand source code @@ -278,18 +282,22 @@

    Functions

    Note the returned values are for the device hardware controller, not the control server. - If no setup argument is provided, the Setup will be loaded from the GlobalState. + If no setup argument is provided, the Setup will be loaded from the Configuration Manager. """ - from egse.state import GlobalState - from egse.setup import SetupError + from egse.setup import SetupError, load_setup - setup = setup or GlobalState.setup + setup = setup or load_setup() try: - hexapod_id = setup.gse.hexapod.device_args.device_id + try: + hexapod_id = setup.gse.hexapod.device_args.device_id + hexapod_name: str = setup.gse.hexapod.device_args.device_name + except AttributeError: + # Handle older Setups where no device_args existed for the PUNA hexapod + hexapod_id = setup.gse.hexapod.ID + hexapod_name: str = setup.gse.hexapod.device_name hexapod_id = f"H_{hexapod_id}" - hexapod_name: str = setup.gse.hexapod.device_args.device_name hostname: str = PUNA_SETTINGS[hexapod_id]["HOSTNAME"] port: int = int(PUNA_SETTINGS[hexapod_id]["PORT"]) controller_type: str = PUNA_SETTINGS[hexapod_id]["TYPE"] diff --git a/docs/api/egse/hexapod/symetrie/puna_cs.html b/docs/api/egse/hexapod/symetrie/puna_cs.html index e56c17c..cbb5120 100644 --- a/docs/api/egse/hexapod/symetrie/puna_cs.html +++ b/docs/api/egse/hexapod/symetrie/puna_cs.html @@ -74,7 +74,6 @@

    Module egse.hexapod.symetrie.puna_cs

    from prometheus_client import start_http_server from egse.control import ControlServer -from egse.hexapod.symetrie.puna import PunaProxy from egse.hexapod.symetrie.puna_protocol import PunaProtocol from egse.settings import Settings @@ -409,10 +408,15 @@

    Inherited members

    • ControlServer:
    • diff --git a/docs/api/egse/hexapod/symetrie/puna_protocol.html b/docs/api/egse/hexapod/symetrie/puna_protocol.html index 805d37c..9d8eebb 100644 --- a/docs/api/egse/hexapod/symetrie/puna_protocol.html +++ b/docs/api/egse/hexapod/symetrie/puna_protocol.html @@ -38,6 +38,7 @@

      Module egse.hexapod.symetrie.puna_protocol

      from egse.hk import read_conversion_dict, convert_hk_names from egse.protocol import CommandProtocol from egse.settings import Settings +from egse.setup import load_setup from egse.system import format_datetime from egse.zmq_ser import bind_address @@ -56,8 +57,10 @@

      Module egse.hexapod.symetrie.puna_protocol

      def __init__(self, control_server: ControlServer): super().__init__() self.control_server = control_server + setup = load_setup() - self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), use_site=True) + self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), use_site=True, + setup=setup) if Settings.simulation_mode(): self.hexapod = PunaSimulator() @@ -222,8 +225,10 @@

      Inherited members

      def __init__(self, control_server: ControlServer): super().__init__() self.control_server = control_server + setup = load_setup() - self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), use_site=True) + self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), use_site=True, + setup=setup) if Settings.simulation_mode(): self.hexapod = PunaSimulator() diff --git a/docs/api/egse/hexapod/symetrie/puna_ui.html b/docs/api/egse/hexapod/symetrie/puna_ui.html index e806de0..7573969 100644 --- a/docs/api/egse/hexapod/symetrie/puna_ui.html +++ b/docs/api/egse/hexapod/symetrie/puna_ui.html @@ -82,7 +82,6 @@

      Module egse.hexapod.symetrie.puna_ui

      from egse.hexapod.symetrie.hexapod_ui import HexapodUIController from egse.hexapod.symetrie.hexapod_ui import HexapodUIModel from egse.hexapod.symetrie.hexapod_ui import HexapodUIView -from egse.hexapod.symetrie.puna import PunaProxy from egse.hexapod.symetrie.puna import PunaSimulator from egse.process import ProcessStatus from egse.resource import get_resource @@ -784,7 +783,7 @@

      Methods

      (device_controller_type: DeviceControllerType, device_id: str)
      -

      QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      +

      QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      Expand source code diff --git a/docs/api/egse/hexapod/symetrie/zonda.html b/docs/api/egse/hexapod/symetrie/zonda.html index 2492cc0..a87eba2 100644 --- a/docs/api/egse/hexapod/symetrie/zonda.html +++ b/docs/api/egse/hexapod/symetrie/zonda.html @@ -350,11 +350,10 @@

      Module egse.hexapod.symetrie.zonda

      try: sc = self.get_general_state() in_position = sc[1][IN_POSITION] - in_motion = sc[1][IN_MOTION] except ZondaError as exc: raise HexapodError("Couldn't Execute command on Hexapod.") from exc - return in_position and not in_motion + return in_position def activate_control_loop(self): try: @@ -1184,11 +1183,10 @@

      Classes

      try: sc = self.get_general_state() in_position = sc[1][IN_POSITION] - in_motion = sc[1][IN_MOTION] except ZondaError as exc: raise HexapodError("Couldn't Execute command on Hexapod.") from exc - return in_position and not in_motion + return in_position def activate_control_loop(self): try: diff --git a/docs/api/egse/hexapod/symetrie/zonda_cs.html b/docs/api/egse/hexapod/symetrie/zonda_cs.html index d91a067..f0a02b1 100644 --- a/docs/api/egse/hexapod/symetrie/zonda_cs.html +++ b/docs/api/egse/hexapod/symetrie/zonda_cs.html @@ -385,10 +385,15 @@

      Inherited members

      • ControlServer:
      • diff --git a/docs/api/egse/hexapod/symetrie/zonda_devif.html b/docs/api/egse/hexapod/symetrie/zonda_devif.html index 83303f6..95a219b 100644 --- a/docs/api/egse/hexapod/symetrie/zonda_devif.html +++ b/docs/api/egse/hexapod/symetrie/zonda_devif.html @@ -29,14 +29,13 @@

        Module egse.hexapod.symetrie.zonda_devif

        import logging
         import socket
         import threading
        -import time
         from telnetlib import Telnet
        -from time import sleep
         from typing import List
         
         import paramiko
        +import time
        +from time import sleep
         
        -from egse.decorators import timer
         from egse.device import DeviceTransport
         from egse.settings import Settings
         from egse.system import wait_until
        diff --git a/docs/api/egse/hexapod/symetrie/zonda_protocol.html b/docs/api/egse/hexapod/symetrie/zonda_protocol.html
        index f513a7a..a79622c 100644
        --- a/docs/api/egse/hexapod/symetrie/zonda_protocol.html
        +++ b/docs/api/egse/hexapod/symetrie/zonda_protocol.html
        @@ -37,6 +37,7 @@ 

        Module egse.hexapod.symetrie.zonda_protocol

        from egse.metrics import define_metrics from egse.protocol import CommandProtocol from egse.settings import Settings +from egse.setup import load_setup from egse.system import format_datetime from egse.zmq_ser import bind_address @@ -53,9 +54,12 @@

        Module egse.hexapod.symetrie.zonda_protocol

        class ZondaProtocol(CommandProtocol): def __init__(self, control_server: ControlServer): super().__init__() + self.control_server = control_server + setup = load_setup() - self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), use_site=True) + self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), use_site=True, + setup=setup) if Settings.simulation_mode(): self.hexapod = ZondaSimulator() @@ -68,7 +72,7 @@

        Module egse.hexapod.symetrie.zonda_protocol

        self.build_device_method_lookup_table(self.hexapod) - self.metrics = define_metrics("ZONDA") + self.metrics = define_metrics("ZONDA", setup=setup) def get_bind_address(self): return bind_address( @@ -96,6 +100,7 @@

        Module egse.hexapod.symetrie.zonda_protocol

        mach_positions = self.hexapod.get_machine_positions() user_positions = self.hexapod.get_user_positions() actuator_length = self.hexapod.get_actuator_length() + actuator_temperature = self.hexapod.get_temperature() # TODO If you change these names, please, also change them in egse.fov.fov_hk! for idx, key in enumerate( @@ -113,6 +118,11 @@

        Module egse.hexapod.symetrie.zonda_protocol

        ): result[key] = actuator_length[idx] + for idx, key in enumerate( + ["atemp_1", "atemp_2", "atemp_3", "atemp_4", "atemp_5", "atemp_6"] + ): + result[key] = actuator_temperature[idx] + # # TODO: # # the get_general_state() method should be refactored as to return a dict instead of a # # list. Also, we might want to rethink the usefulness of returning the tuple, @@ -203,9 +213,12 @@

        Inherited members

        class ZondaProtocol(CommandProtocol):
             def __init__(self, control_server: ControlServer):
                 super().__init__()
        +
                 self.control_server = control_server
        +        setup = load_setup()
         
        -        self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), use_site=True)
        +        self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), use_site=True,
        +                                                        setup=setup)
         
                 if Settings.simulation_mode():
                     self.hexapod = ZondaSimulator()
        @@ -218,7 +231,7 @@ 

        Inherited members

        self.build_device_method_lookup_table(self.hexapod) - self.metrics = define_metrics("ZONDA") + self.metrics = define_metrics("ZONDA", setup=setup) def get_bind_address(self): return bind_address( @@ -246,6 +259,7 @@

        Inherited members

        mach_positions = self.hexapod.get_machine_positions() user_positions = self.hexapod.get_user_positions() actuator_length = self.hexapod.get_actuator_length() + actuator_temperature = self.hexapod.get_temperature() # TODO If you change these names, please, also change them in egse.fov.fov_hk! for idx, key in enumerate( @@ -263,6 +277,11 @@

        Inherited members

        ): result[key] = actuator_length[idx] + for idx, key in enumerate( + ["atemp_1", "atemp_2", "atemp_3", "atemp_4", "atemp_5", "atemp_6"] + ): + result[key] = actuator_temperature[idx] + # # TODO: # # the get_general_state() method should be refactored as to return a dict instead of a # # list. Also, we might want to rethink the usefulness of returning the tuple, diff --git a/docs/api/egse/hexapod/symetrie/zonda_ui.html b/docs/api/egse/hexapod/symetrie/zonda_ui.html index 16fa731..0d21ab6 100644 --- a/docs/api/egse/hexapod/symetrie/zonda_ui.html +++ b/docs/api/egse/hexapod/symetrie/zonda_ui.html @@ -861,7 +861,7 @@

        Methods

        class ZondaUIView
        -

        QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

        +

        QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

        Expand source code diff --git a/docs/api/egse/hk.html b/docs/api/egse/hk.html index 54e512c..e175cff 100644 --- a/docs/api/egse/hk.html +++ b/docs/api/egse/hk.html @@ -29,22 +29,24 @@

        Module egse.hk

        import csv
         import datetime
         import logging
        -import os
         from enum import Enum
         from pathlib import Path
        -from typing import Union
        +from typing import Union, Optional
         
         import dateutil.parser as date_parser
         import numpy as np
         
         from egse.config import find_files
         from egse.env import get_data_storage_location
        -from egse.obsid import ObservationIdentifier, obsid_from_storage
        +from egse.obsid import ObservationIdentifier
        +from egse.obsid import obsid_from_storage
         from egse.settings import Settings
        -from egse.setup import SetupError
        -from egse.state import GlobalState
        -from egse.system import read_last_line, read_last_lines, SECONDS_IN_A_DAY, str_to_datetime, \
        -    time_since_epoch_1958
        +from egse.setup import SetupError, load_setup, Setup
        +from egse.system import SECONDS_IN_A_DAY
        +from egse.system import read_last_line
        +from egse.system import read_last_lines
        +from egse.system import str_to_datetime
        +from egse.system import time_since_epoch_1958
         
         logger = logging.getLogger(__name__)
         
        @@ -84,13 +86,12 @@ 

        Module egse.hk

        class HKError(Exception): - """ An HK-specific error.""" - + """An HK-specific error.""" pass def get_housekeeping(hk_name: str, obsid: Union[ObservationIdentifier, str, int] = None, od: str = None, - time_window: int = None, data_dir=None): + time_window: int = None, data_dir=None, setup: Optional[Setup] = None): """ Returns the timestamp(s) and housekeeping value(s) for the housekeeping parameter with the given name. @@ -117,6 +118,7 @@

        Module egse.hk

        the moment this method is called. If not given, the latest housekeeping value is returned. data_dir: Folder (with sub-folders /daily and /obs) in which the HK files are stored. If this argument is not provided, the data_dir will be determined from the environment variable PLATO_DATA_STORAGE_LOCATION. + setup: Setup Raises: A HKError when one of the following problems occur @@ -130,6 +132,8 @@

        Module egse.hk

        """ + setup = setup or load_setup() + # Either specify the obsid or the OD (or neither of them) but not both if obsid is not None and od is not None: @@ -143,7 +147,7 @@

        Module egse.hk

        if obsid: try: - return _get_housekeeping_obsid(hk_name, data_dir, obsid=obsid, time_window=time_window) + return _get_housekeeping_obsid(hk_name, data_dir, obsid=obsid, time_window=time_window, setup=setup) except (ValueError, StopIteration, FileNotFoundError) as exc: raise HKError(f"No HK found for {hk_name} for obsid {obsid} at {SITE_ID}") from exc @@ -152,14 +156,14 @@

        Module egse.hk

        if od: try: - return _get_housekeeping_od(hk_name, data_dir, od=od, time_window=time_window) + return _get_housekeeping_od(hk_name, data_dir, od=od, time_window=time_window, setup=setup) except (ValueError, StopIteration, FileNotFoundError) as exc: raise HKError(f"No HK found for {hk_name} for OD {od} at {SITE_ID}") from exc # Didn't specify neither the obsid nor the OD try: - return _get_housekeeping_daily(hk_name, data_dir, time_window=time_window) + return _get_housekeeping_daily(hk_name, data_dir, time_window=time_window, setup=setup) except (ValueError, StopIteration, FileNotFoundError) as exc: raise HKError(f"No HK found for {hk_name} for today at {SITE_ID}") from exc @@ -258,7 +262,7 @@

        Module egse.hk

        return timestamp_array, hk_array -def _get_housekeeping_od(hk_name: str, data_dir, od: str, time_window: int = None): +def _get_housekeeping_od(hk_name: str, data_dir, od: str, time_window: int = None, setup: Optional[Setup] = None): """ Return the timestamp(s) and HK value(s) for the HK parameter with the given name, for the given OD. When the time window has not been specified, the last timestamp and HK value will be returned for the given OD. @@ -277,6 +281,7 @@

        Module egse.hk

        - od: Identifier for the OD (yyyymmdd). - time_window: Length of the time window over which to retrieve the housekeeping [s]. The time window ends at the moment this method is called. If not given, the latest HK-value is returned. + - setup: Setup. Returns: - If the time window has not been specified: the most recent timestamp and HK value. @@ -284,11 +289,12 @@

        Module egse.hk

        specified time window. """ + setup = setup or load_setup() hk_dir = f"{data_dir}/daily/" # Where the HK is stored try: - origin, timestamp_name = get_hk_info(hk_name) + origin, timestamp_name = get_hk_info(hk_name, setup=setup) except KeyError: @@ -301,7 +307,7 @@

        Module egse.hk

        def _get_housekeeping_obsid(hk_name: str, data_dir, obsid: Union[ObservationIdentifier, str, int], - time_window: int = None): + time_window: int = None, setup: Optional[Setup] = None): """ Return the timestamp(s) and HK value(s) for the HK parameter with the given name, for the given obsid. When the time window has not been specified, the last timestamp and HK value will be returned for the given obsid. @@ -321,6 +327,7 @@

        Module egse.hk

        TEST_LAB_SETUP, or an integer representing the test ID. - time_window: Length of the time window over which to retrieve the housekeeping [s]. The time window ends at the moment this method is called. If not given, the latest HK-value is returned. + - setup: Setup. Returns: - If the time window has not been specified: the most recent timestamp and HK value. @@ -328,11 +335,13 @@

        Module egse.hk

        specified time window. """ + setup = setup or load_setup() + hk_dir = f"{data_dir}/obs/" # Where the HK is stored try: - origin, timestamp_name = get_hk_info(hk_name) + origin, timestamp_name = get_hk_info(hk_name, setup=setup) except KeyError: @@ -353,7 +362,7 @@

        Module egse.hk

        return _get_housekeeping(hk_name, timestamp_name, hk_dir, hk_files, time_window=time_window) -def _get_housekeeping_daily(hk_name: str, data_dir, time_window: int = None): +def _get_housekeeping_daily(hk_name: str, data_dir, time_window: int = None, setup: Optional[Setup] = None): """ Return the timestamp(s) and HK value(s) for the HK parameter with the given name. When the time window has not been specified, the last timestamp and HK value will be returned. It is possible that @@ -380,6 +389,7 @@

        Module egse.hk

        - data_dir: Folder (with sub-folders /daily and /obs) in which the HK files are stored. - time_window: Length of the time window over which to retrieve the housekeeping [s]. The time window ends at the moment this method is called. If not given, the latest HK-value is returned. + - setup: Setup. Returns: - If the time window has not been specified: the most recent timestamp and HK value. @@ -387,11 +397,12 @@

        Module egse.hk

        specified time window. """ + setup = setup or load_setup() hk_dir = f"{data_dir}/daily/" # Where the HK is stored try: - origin, timestamp_name = get_hk_info(hk_name) + origin, timestamp_name = get_hk_info(hk_name, setup=setup) except KeyError: @@ -637,33 +648,36 @@

        Module egse.hk

        def convert_hk_names(original_hk: dict, conversion_dict: dict) -> dict: - """ Converts the names of the HK parameters in the given dictionary. + """ + Converts the names of the HK parameters in the given dictionary. - The names in the given dictionary of HK parameters are replaced by the names from the given conversion dictionary. + The names/keys in the given dictionary of HK parameters (original_hk) are replaced by the names + from the given conversion dictionary. The original dictionary is left unchanged, a new dictionary is + returned. Args: - original_hk: Original dictionary of HK parameters. - - conversion_dict: Dictionary with the original HK names as keys and the new HK names as keys. + - conversion_dict: Dictionary with the original HK names as keys and the new HK names as values. - Returns: Dictionary of HK parameters with the corrected HK names. + Returns: + A new dictionary of HK parameters with the corrected HK names. """ - old_keys = list(original_hk.keys()) converted_hk = {} - for old_key in old_keys: + for orig_key in original_hk: try: - new_key = conversion_dict[old_key] + new_key = conversion_dict[orig_key] except KeyError: - new_key = old_key # No need to convert the name of the timestamp column(s) + new_key = orig_key # no conversion, just copy the key:value pair - converted_hk[new_key] = original_hk[old_key] + converted_hk[new_key] = original_hk[orig_key] return converted_hk -def read_conversion_dict(storage_mnemonic: str, use_site: bool = False): +def read_conversion_dict(storage_mnemonic: str, use_site: bool = False, setup: Optional[Setup] = None): """ Read the HK spreadsheet and compose conversion dictionary for HK names. The spreadsheet contains the following information: @@ -676,12 +690,15 @@

        Module egse.hk

        Args: - storage_mnemonic: Storage mnemonic of the component for which to compose the conversion dictionary - use_site: Indicate whether or not the prefixes of the new HK names are TH-specific + - setup: Setup. Returns: Dictionary with the original HK names as keys and the converted HK names as values. """ + setup = setup or load_setup() + try: - hk_info_table = GlobalState.setup.telemetry.dictionary + hk_info_table = setup.telemetry.dictionary except AttributeError: raise SetupError("Version of the telemetry dictionary not specified in the current setup") @@ -711,7 +728,7 @@

        Module egse.hk

        return dict(zip(original_name_col, correct_name_col)) -def get_hk_info(hk_name: str): +def get_hk_info(hk_name: str, setup: Optional[Setup] = None): """ Read the HK spreadsheet and extract information for the given HK parameter. The spreadsheet contains the following information: @@ -723,13 +740,15 @@

        Module egse.hk

        Args: - hk_name: Name of the HK parameter. + - setup: Setup. Returns: - storage mnemonic of the component that generates the given HK parameter - name of the column in the HK file with the corresponding timestamp """ - hk_info_table = GlobalState.setup.telemetry.dictionary + setup = setup or load_setup() + hk_info_table = setup.telemetry.dictionary storage_mnemonic = hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values hk_names = hk_info_table[TmDictionaryColumns.CORRECT_HK_NAMES].values @@ -743,18 +762,24 @@

        Module egse.hk

        raise HKError(f"HK parameter {hk_name} unknown") -def get_storage_mnemonics(): +def get_storage_mnemonics(setup: Setup = None): """ Return the list of the storage mnemonics from the TM dictionary. + Args: + - setup: Setup. + Returns: List of the storage mnemonics from the TM dictionary. """ - hk_info_table = GlobalState.setup.telemetry.dictionary + setup = setup or load_setup() + + hk_info_table = setup.telemetry.dictionary storage_mnemonics = hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values + return np.unique(storage_mnemonics) -def get_housekeeping_names(name_filter=None, device_filter=None): +def get_housekeeping_names(name_filter=None, device_filter=None, setup: Setup = None): """ Return HK names, storage mnemonic, and description. The TM dictionary is read into a Pandas DataFrame. If a device filter is given, only the rows pertaining to the @@ -766,17 +791,22 @@

        Module egse.hk

        - "Storage mnemonic": Storage mnemonic of the device producing the HK; - "Description": Description of the HK parameter. - Synopsis: get_housekeeping_names(name_filter="RAW", device_filter="N-FEE-HK") + Synopsis: + - get_housekeeping_names(name_filter="RAW", device_filter="N-FEE-HK") + - get_housekeeping_names(name_filter="RAW", device_filter="N-FEE-HK", setup=setup) Args: - name_filter: Filter the HK dataframe, based on (a part of) the name of the HK parameter(s) - device: Filter the HK dataframe, based on the given storage mmenmonic + - setup: Setup. Returns: Pandas DataFrame with the HK name, storage mnemonic, and description of the HK parameters that pass the given filter. """ - hk_info_table = GlobalState.setup.telemetry.dictionary + setup = setup or load_setup() + + hk_info_table = setup.telemetry.dictionary hk_info_table.dropna(subset=[TmDictionaryColumns.CORRECT_HK_NAMES], inplace=True) if device_filter: @@ -801,46 +831,52 @@

        Functions

        Converts the names of the HK parameters in the given dictionary.

        -

        The names in the given dictionary of HK parameters are replaced by the names from the given conversion dictionary.

        +

        The names/keys in the given dictionary of HK parameters (original_hk) are replaced by the names +from the given conversion dictionary. The original dictionary is left unchanged, a new dictionary is +returned.

        Args

        • original_hk: Original dictionary of HK parameters.
        • -
        • conversion_dict: Dictionary with the original HK names as keys and the new HK names as keys. -Returns: Dictionary of HK parameters with the corrected HK names.
        • -
        +
      • conversion_dict: Dictionary with the original HK names as keys and the new HK names as values.
      • +
      +

      Returns

      +

      A new dictionary of HK parameters with the corrected HK names.

    Expand source code
    def convert_hk_names(original_hk: dict, conversion_dict: dict) -> dict:
    -    """ Converts the names of the HK parameters in the given dictionary.
    +    """
    +    Converts the names of the HK parameters in the given dictionary.
     
    -    The names in the given dictionary of HK parameters are replaced by the names from the given conversion dictionary.
    +    The names/keys in the given dictionary of HK parameters (original_hk) are replaced by the names
    +    from the given conversion dictionary. The original dictionary is left unchanged, a new dictionary is
    +    returned.
     
         Args:
             - original_hk: Original dictionary of HK parameters.
    -        - conversion_dict: Dictionary with the original HK names as keys and the new HK names as keys.
    +        - conversion_dict: Dictionary with the original HK names as keys and the new HK names as values.
     
    -    Returns: Dictionary of HK parameters with the corrected HK names.
    +    Returns:
    +        A new dictionary of HK parameters with the corrected HK names.
         """
     
    -    old_keys = list(original_hk.keys())
         converted_hk = {}
     
    -    for old_key in old_keys:
    +    for orig_key in original_hk:
     
             try:
    -            new_key = conversion_dict[old_key]
    +            new_key = conversion_dict[orig_key]
             except KeyError:
    -            new_key = old_key   # No need to convert the name of the timestamp column(s)
    +            new_key = orig_key   # no conversion, just copy the key:value pair
     
    -        converted_hk[new_key] = original_hk[old_key]
    +        converted_hk[new_key] = original_hk[orig_key]
     
         return converted_hk
    -def get_hk_info(hk_name: str) +def get_hk_info(hk_name: str, setup: Optional[Setup] = None)

    Read the HK spreadsheet and extract information for the given HK parameter.

    @@ -853,6 +889,7 @@

    Args

    Args

    • hk_name: Name of the HK parameter.
    • +
    • setup: Setup.

    Returns

      @@ -863,7 +900,7 @@

      Returns

      Expand source code -
      def get_hk_info(hk_name: str):
      +
      def get_hk_info(hk_name: str, setup: Optional[Setup] = None):
           """ Read the HK spreadsheet and extract information for the given HK parameter.
       
           The spreadsheet contains the following information:
      @@ -875,13 +912,15 @@ 

      Returns

      Args: - hk_name: Name of the HK parameter. + - setup: Setup. Returns: - storage mnemonic of the component that generates the given HK parameter - name of the column in the HK file with the corresponding timestamp """ - hk_info_table = GlobalState.setup.telemetry.dictionary + setup = setup or load_setup() + hk_info_table = setup.telemetry.dictionary storage_mnemonic = hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values hk_names = hk_info_table[TmDictionaryColumns.CORRECT_HK_NAMES].values @@ -896,7 +935,7 @@

      Returns

    -def get_housekeeping(hk_name: str, obsid: Union[ObservationIdentifier, str, int] = None, od: str = None, time_window: int = None, data_dir=None) +def get_housekeeping(hk_name: str, obsid: Union[ObservationIdentifier, str, int] = None, od: str = None, time_window: int = None, data_dir=None, setup: Optional[Setup] = None)

    Returns the timestamp(s) and housekeeping value(s) for the housekeeping parameter with the given name.

    @@ -932,6 +971,8 @@

    Args

    data_dir
    Folder (with sub-folders /daily and /obs) in which the HK files are stored. If this argument is not provided, the data_dir will be determined from the environment variable PLATO_DATA_STORAGE_LOCATION.
    +
    setup
    +
    Setup

    Raises

    A HKError when one of the following problems occur @@ -948,7 +989,7 @@

    Returns

    Expand source code
    def get_housekeeping(hk_name: str, obsid: Union[ObservationIdentifier, str, int] = None, od: str = None,
    -                     time_window: int = None, data_dir=None):
    +                     time_window: int = None, data_dir=None, setup: Optional[Setup] = None):
         """
         Returns the timestamp(s) and housekeeping value(s) for the housekeeping parameter with the given name.
     
    @@ -975,6 +1016,7 @@ 

    Returns

    the moment this method is called. If not given, the latest housekeeping value is returned. data_dir: Folder (with sub-folders /daily and /obs) in which the HK files are stored. If this argument is not provided, the data_dir will be determined from the environment variable PLATO_DATA_STORAGE_LOCATION. + setup: Setup Raises: A HKError when one of the following problems occur @@ -988,6 +1030,8 @@

    Returns

    """ + setup = setup or load_setup() + # Either specify the obsid or the OD (or neither of them) but not both if obsid is not None and od is not None: @@ -1001,7 +1045,7 @@

    Returns

    if obsid: try: - return _get_housekeeping_obsid(hk_name, data_dir, obsid=obsid, time_window=time_window) + return _get_housekeeping_obsid(hk_name, data_dir, obsid=obsid, time_window=time_window, setup=setup) except (ValueError, StopIteration, FileNotFoundError) as exc: raise HKError(f"No HK found for {hk_name} for obsid {obsid} at {SITE_ID}") from exc @@ -1010,20 +1054,20 @@

    Returns

    if od: try: - return _get_housekeeping_od(hk_name, data_dir, od=od, time_window=time_window) + return _get_housekeeping_od(hk_name, data_dir, od=od, time_window=time_window, setup=setup) except (ValueError, StopIteration, FileNotFoundError) as exc: raise HKError(f"No HK found for {hk_name} for OD {od} at {SITE_ID}") from exc # Didn't specify neither the obsid nor the OD try: - return _get_housekeeping_daily(hk_name, data_dir, time_window=time_window) + return _get_housekeeping_daily(hk_name, data_dir, time_window=time_window, setup=setup) except (ValueError, StopIteration, FileNotFoundError) as exc: raise HKError(f"No HK found for {hk_name} for today at {SITE_ID}") from exc
    -def get_housekeeping_names(name_filter=None, device_filter=None) +def get_housekeeping_names(name_filter=None, device_filter=None, setup: Setup = None)

    Return HK names, storage mnemonic, and description.

    @@ -1036,11 +1080,16 @@

    Returns

    - "CAM EGSE mnemonic": Name of the HK parameter; - "Storage mnemonic": Storage mnemonic of the device producing the HK; - "Description": Description of the HK parameter.

    -

    Synopsis: get_housekeeping_names(name_filter="RAW", device_filter="N-FEE-HK")

    +

    Synopsis

    +
      +
    • get_housekeeping_names(name_filter="RAW", device_filter="N-FEE-HK")
    • +
    • get_housekeeping_names(name_filter="RAW", device_filter="N-FEE-HK", setup=setup)
    • +

    Args

    • name_filter: Filter the HK dataframe, based on (a part of) the name of the HK parameter(s)
    • -
    • device: Filter the HK dataframe, based on the given storage mmenmonic +
    • device: Filter the HK dataframe, based on the given storage mmenmonic
    • +
    • setup: Setup. Returns: Pandas DataFrame with the HK name, storage mnemonic, and description of the HK parameters that pass the given filter.
    @@ -1048,7 +1097,7 @@

    Args

    Expand source code -
    def get_housekeeping_names(name_filter=None, device_filter=None):
    +
    def get_housekeeping_names(name_filter=None, device_filter=None, setup: Setup = None):
         """ Return HK names, storage mnemonic, and description.
     
         The TM dictionary is read into a Pandas DataFrame.  If a device filter is given, only the rows pertaining to the
    @@ -1060,17 +1109,22 @@ 

    Args

    - "Storage mnemonic": Storage mnemonic of the device producing the HK; - "Description": Description of the HK parameter. - Synopsis: get_housekeeping_names(name_filter="RAW", device_filter="N-FEE-HK") + Synopsis: + - get_housekeeping_names(name_filter="RAW", device_filter="N-FEE-HK") + - get_housekeeping_names(name_filter="RAW", device_filter="N-FEE-HK", setup=setup) Args: - name_filter: Filter the HK dataframe, based on (a part of) the name of the HK parameter(s) - device: Filter the HK dataframe, based on the given storage mmenmonic + - setup: Setup. Returns: Pandas DataFrame with the HK name, storage mnemonic, and description of the HK parameters that pass the given filter. """ - hk_info_table = GlobalState.setup.telemetry.dictionary + setup = setup or load_setup() + + hk_info_table = setup.telemetry.dictionary hk_info_table.dropna(subset=[TmDictionaryColumns.CORRECT_HK_NAMES], inplace=True) if device_filter: @@ -1266,28 +1320,38 @@

    Args

    -def get_storage_mnemonics() +def get_storage_mnemonics(setup: Setup = None)

    Return the list of the storage mnemonics from the TM dictionary.

    -

    Returns: List of the storage mnemonics from the TM dictionary.

    +

    Args

    +
      +
    • setup: Setup. +Returns: List of the storage mnemonics from the TM dictionary.
    • +
    Expand source code -
    def get_storage_mnemonics():
    +
    def get_storage_mnemonics(setup: Setup = None):
         """ Return the list of the storage mnemonics from the TM dictionary.
     
    +    Args:
    +          - setup: Setup.
    +
         Returns: List of the storage mnemonics from the TM dictionary.
         """
     
    -    hk_info_table = GlobalState.setup.telemetry.dictionary
    +    setup = setup or load_setup()
    +
    +    hk_info_table = setup.telemetry.dictionary
         storage_mnemonics = hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values
    +
         return np.unique(storage_mnemonics)
    -def read_conversion_dict(storage_mnemonic: str, use_site: bool = False) +def read_conversion_dict(storage_mnemonic: str, use_site: bool = False, setup: Optional[Setup] = None)

    Read the HK spreadsheet and compose conversion dictionary for HK names.

    @@ -1300,14 +1364,15 @@

    Args

    Args

    • storage_mnemonic: Storage mnemonic of the component for which to compose the conversion dictionary
    • -
    • use_site: Indicate whether or not the prefixes of the new HK names are TH-specific +
    • use_site: Indicate whether or not the prefixes of the new HK names are TH-specific
    • +
    • setup: Setup. Returns: Dictionary with the original HK names as keys and the converted HK names as values.
    Expand source code -
    def read_conversion_dict(storage_mnemonic: str, use_site: bool = False):
    +
    def read_conversion_dict(storage_mnemonic: str, use_site: bool = False, setup: Optional[Setup] = None):
         """ Read the HK spreadsheet and compose conversion dictionary for HK names.
     
         The spreadsheet contains the following information:
    @@ -1320,12 +1385,15 @@ 

    Args

    Args: - storage_mnemonic: Storage mnemonic of the component for which to compose the conversion dictionary - use_site: Indicate whether or not the prefixes of the new HK names are TH-specific + - setup: Setup. Returns: Dictionary with the original HK names as keys and the converted HK names as values. """ + setup = setup or load_setup() + try: - hk_info_table = GlobalState.setup.telemetry.dictionary + hk_info_table = setup.telemetry.dictionary except AttributeError: raise SetupError("Version of the telemetry dictionary not specified in the current setup") @@ -1371,8 +1439,7 @@

    Classes

    Expand source code
    class HKError(Exception):
    -    """ An HK-specific error."""
    -
    +    """An HK-specific error."""
         pass

    Ancestors

    diff --git a/docs/api/egse/index.html b/docs/api/egse/index.html index 7c6a3d6..fce19f3 100644 --- a/docs/api/egse/index.html +++ b/docs/api/egse/index.html @@ -88,10 +88,6 @@

    Sub-modules

    -
    egse.csl
    -
    -
    -
    egse.das

    The Data Acquisition System (DAS) is a small application that performs measurements on @@ -189,9 +185,10 @@

    Sub-modules

    -
    egse.lib
    +
    egse.listener
    -
    +

    This module defines Listeners between control servers and is part of a notification system where changes in +one control-server are notified to all …

    egse.logger
    @@ -211,6 +208,10 @@

    Sub-modules

    +
    egse.ni
    +
    +
    +
    egse.obs_inspection
    @@ -269,6 +270,10 @@

    Sub-modules

    This module provides convenience functions to use resources in your code without the need to specify an absolute path or try to locate the resources …

    +
    egse.rmap
    +
    +
    +
    egse.rst
    @@ -416,7 +421,6 @@

    Index

  • egse.confman
  • egse.control
  • egse.coordinates
  • -
  • egse.csl
  • egse.das
  • egse.decorators
  • egse.device
  • @@ -440,11 +444,12 @@

    Index

  • egse.hk
  • egse.imageviewer
  • egse.lampcontrol
  • -
  • egse.lib
  • +
  • egse.listener
  • egse.logger
  • egse.metrics
  • egse.mixin
  • egse.monitoring
  • +
  • egse.ni
  • egse.obs_inspection
  • egse.observer
  • egse.obsid
  • @@ -458,6 +463,7 @@

    Index

  • egse.reload
  • egse.reprocess
  • egse.resource
  • +
  • egse.rmap
  • egse.rst
  • egse.search
  • egse.serialdevice
  • diff --git a/docs/api/egse/lampcontrol/beaglebone/beaglebone_cs.html b/docs/api/egse/lampcontrol/beaglebone/beaglebone_cs.html index be4992e..e301e2f 100644 --- a/docs/api/egse/lampcontrol/beaglebone/beaglebone_cs.html +++ b/docs/api/egse/lampcontrol/beaglebone/beaglebone_cs.html @@ -307,10 +307,15 @@

    Inherited members

    • ControlServer:
    • diff --git a/docs/api/egse/lampcontrol/energetiq/lampEQ99_cs.html b/docs/api/egse/lampcontrol/energetiq/lampEQ99_cs.html index 0b9c991..5a36426 100644 --- a/docs/api/egse/lampcontrol/energetiq/lampEQ99_cs.html +++ b/docs/api/egse/lampcontrol/energetiq/lampEQ99_cs.html @@ -314,10 +314,15 @@

      Inherited members

      • ControlServer:
      • diff --git a/docs/api/egse/lampcontrol/energetiq/lampEQ99_protocol.html b/docs/api/egse/lampcontrol/energetiq/lampEQ99_protocol.html index ffec52f..c8e9d06 100644 --- a/docs/api/egse/lampcontrol/energetiq/lampEQ99_protocol.html +++ b/docs/api/egse/lampcontrol/energetiq/lampEQ99_protocol.html @@ -33,6 +33,7 @@

        Module egse.lampcontrol.energetiq.lampEQ99_protocolModule egse.lampcontrol.energetiq.lampEQ99_protocolModule egse.lampcontrol.energetiq.lampEQ99_protocolClasses

        def __init__(self, control_server: ControlServer): super().__init__() self.control_server = control_server + setup = load_setup() if Settings.simulation_mode(): self.lamp = LampEQ99Simulator() @@ -137,7 +140,7 @@

        Classes

        self.build_device_method_lookup_table(self.lamp) - self.metrics = define_metrics("EQ99") + self.metrics = define_metrics("EQ99", use_site=False, setup=setup) self.synoptics = SynopticsManagerProxy() def get_bind_address(self): diff --git a/docs/api/egse/lampcontrol/energetiq/lampEQ99_ui.html b/docs/api/egse/lampcontrol/energetiq/lampEQ99_ui.html index 38f867a..025b7cc 100644 --- a/docs/api/egse/lampcontrol/energetiq/lampEQ99_ui.html +++ b/docs/api/egse/lampcontrol/energetiq/lampEQ99_ui.html @@ -1042,7 +1042,7 @@

        Methods

        class LampUIView
        -

        QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

        +

        QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

        Expand source code diff --git a/docs/api/egse/lib/index.html b/docs/api/egse/lib/index.html index f4e34d0..d7ed473 100644 --- a/docs/api/egse/lib/index.html +++ b/docs/api/egse/lib/index.html @@ -19,18 +19,19 @@
        -

        Namespace egse.lib

        +

        Module egse.lib

        +
        + +Expand source code + +
        __pdoc__ = {
        +    "xximc": False,
        +}
        +
        -

        Sub-modules

        -
        -
        egse.lib.ximc
        -
        -
        -
        -
        @@ -50,11 +51,6 @@

        Index

      • egse
      -
    • Sub-modules

      - -
    diff --git a/docs/api/egse/lib/ximc/index.html b/docs/api/egse/lib/ximc/index.html index 1624d36..811379f 100644 --- a/docs/api/egse/lib/ximc/index.html +++ b/docs/api/egse/lib/ximc/index.html @@ -19,18 +19,19 @@
    diff --git a/docs/api/egse/listener.html b/docs/api/egse/listener.html new file mode 100644 index 0000000..545c7ff --- /dev/null +++ b/docs/api/egse/listener.html @@ -0,0 +1,734 @@ + + + + + + +egse.listener API documentation + + + + + + + + + + + +
    +
    +
    +

    Module egse.listener

    +
    +
    +

    This module defines Listeners between control servers and is part of a notification system where changes in +one control-server are notified to all listeners of that control-server.

    +

    Since we have communication between control servers, a listener registers to a control server with its name/id +and a Proxy class that will be used to send the notification. A listener can also contain an event_id to restrict +events to handle. (Shall events be filtered on the server (only notify events that match the event_id or shall they +be filtered on the client (only handle those events that match the event_id ?)

    +

    Any control server will support notification and listeners are added through the service proxy of the control server.

    +

    A control server that wants to be notified by an event needs to implement the EventInterface in the Proxy and in the +Controller.

    +
    + +Expand source code + +
    """
    +This module defines Listeners between control servers and is part of a notification system where changes in
    +one control-server are notified to all listeners of that control-server.
    +
    +Since we have communication between control servers, a listener registers to a control server with its name/id
    +and a Proxy class that will be used to send the notification. A listener can also contain an `event_id` to restrict
    +events to handle. (Shall events be filtered on the server (only notify events that match the `event_id` or shall they
    +be filtered on the client (only handle those events that match the `event_id` ?)
    +
    +Any control server will support notification and listeners are added through the service proxy of the control server.
    +
    +A control server that wants to be notified by an event needs to implement the EventInterface in the Proxy and in the
    +Controller.
    +
    +"""
    +import logging
    +from enum import IntEnum
    +from typing import Any
    +from typing import Dict
    +from typing import List
    +from typing import Type
    +
    +from egse.decorators import dynamic_interface
    +from egse.system import Timer
    +
    +LOGGER = logging.getLogger(__name__)
    +
    +
    +class EVENT_ID(IntEnum):
    +    ALL = 0
    +    SETUP = 1
    +
    +
    +class Event:
    +    def __init__(self, event_id: int, context: Any):
    +        self.id = event_id
    +        self.context = context
    +
    +    def __repr__(self):
    +        return f"Event({EVENT_ID(self.id).name}, {self.context})"
    +
    +    @property
    +    def type(self) -> str:
    +        try:
    +            return self.context['event_type']
    +        except KeyError:
    +            return "unknown: event_type not provided"
    +
    +
    +class EventInterface:
    +    """
    +    A dynamic interface for handling events.
    +
    +    This interface defines a single method, 'handle_event', which is intended
    +    to be implemented by classes that want to handle specific types of events.
    +
    +    Use this interface as a mixin for classes (Proxy/Controller) that implement this `handle_event` method.
    +
    +    """
    +    @dynamic_interface
    +    def handle_event(self, event: Event):
    +        """Handles the specified event.
    +
    +        Args:
    +            event (Event): An instance of the Event class representing the event to be handled.
    +        """
    +        ...
    +
    +
    +class Listeners:
    +    """
    +    A class for managing and notifying registered listeners.
    +
    +    This class provides methods to add, remove, and notify listeners of events.
    +    """
    +
    +    def __init__(self):
    +        """Initializes an instance of the Listeners class."""
    +        self._listeners: Dict[str, dict] = {}
    +
    +    def __len__(self):
    +        """Returns the number of registered listeners."""
    +        return len(self._listeners)
    +
    +    def add_listener(self, listener: dict):
    +        """
    +        Adds a new listener to the registered listeners.
    +
    +        The listener argument dictionary is expected to have at least the following key:values pairs:
    +
    +        * 'name': the name or identifier of the listener
    +        * 'proxy': a Proxy object that will be used for notifying the service
    +
    +        Args:
    +            listener (dict): A dictionary with properties of the listener,
    +                including 'name' and 'proxy'.
    +
    +        Raises:
    +            ValueError: If the listener already exists.
    +        """
    +        try:
    +            listener_name = listener["name"]
    +        except KeyError as exc:
    +            raise ValueError(f"Expected 'name' key in listener argument {listener}.") from exc
    +
    +        if listener_name in self._listeners:
    +            raise ValueError(f"Process {listener_name} is already registered as a listener.")
    +
    +        # For now we make these listeners have a Proxy class that will be used for notification. Later on
    +        # we might have other mechanisms for notification.
    +
    +        from egse.proxy import Proxy
    +
    +        proxy = listener.get('proxy')
    +        if proxy is not None:
    +            if not isinstance(proxy, type) or not issubclass(proxy, Proxy):
    +                raise ValueError(f"Expected 'proxy' in listener argument {proxy=} to be a Proxy sub-class.")
    +
    +        self._listeners[listener_name] = listener
    +
    +    def remove_listener(self, listener: dict):
    +        """
    +        Removes a listener from the registered listeners.
    +
    +        Args:
    +           listener (dict): A dictionary representing the listener to be
    +               removed. It should contain a 'name' key.
    +
    +        Raises:
    +           ValueError: If the 'name' key is not present in the listener
    +               argument or if the specified listener is not registered.
    +        """
    +
    +        try:
    +            listener_name = listener["name"]
    +        except KeyError as exc:
    +            raise ValueError(f"Expected 'name' key in listener argument {listener}.") from exc
    +
    +        try:
    +            del self._listeners[listener_name]
    +        except KeyError as exc:
    +            raise ValueError(f"Process {listener_name} cannot be removed, not registered.") from exc
    +
    +    def notify_listeners(self, event: Event):
    +        """
    +        Notifies all registered listeners of a specific event.
    +
    +        Args:
    +            event (Event): An instance of the Event class representing the
    +                event to be broadcasted.
    +
    +        Note:
    +            The 'handle_event' method is called on each listener's proxy
    +            object to process the event.
    +        """
    +
    +        LOGGER.debug(f"Notifying listeners {self.get_listener_names()}")
    +
    +        for name, listener in self._listeners.items():
    +            self.notify_listener(name, listener, event)
    +
    +    def notify_listener(self, name: str, listener: dict, event: Event):
    +        """
    +        Notifies a registered listener fro the given event.
    +        """
    +        proxy: Type[EventInterface] = listener['proxy']
    +
    +        # The Proxy uses a REQ-REP protocol and waits for an answer. That's why we have a Timer
    +        # in the context so we are able to monitor how long it takes to notify a listener.
    +        # The Timer can be removed when we are confident all `handle_event` functions are properly
    +        # implemented.
    +        with Timer(f"Notify listener {name}", log_level=logging.DEBUG), proxy() as pobj:
    +            rc = pobj.handle_event(event)
    +
    +        LOGGER.debug(f"Listener {name} returned {rc=}")
    +
    +    def get_listener_names(self) -> List[str]:
    +        """Returns a list with the names of the registered listeners."""
    +        return list(self._listeners.keys())
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class EVENT_ID +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +
    +

    An enumeration.

    +
    + +Expand source code + +
    class EVENT_ID(IntEnum):
    +    ALL = 0
    +    SETUP = 1
    +
    +

    Ancestors

    +
      +
    • enum.IntEnum
    • +
    • builtins.int
    • +
    • enum.Enum
    • +
    +

    Class variables

    +
    +
    var ALL
    +
    +
    +
    +
    var SETUP
    +
    +
    +
    +
    +
    +
    +class Event +(event_id: int, context: Any) +
    +
    +
    +
    + +Expand source code + +
    class Event:
    +    def __init__(self, event_id: int, context: Any):
    +        self.id = event_id
    +        self.context = context
    +
    +    def __repr__(self):
    +        return f"Event({EVENT_ID(self.id).name}, {self.context})"
    +
    +    @property
    +    def type(self) -> str:
    +        try:
    +            return self.context['event_type']
    +        except KeyError:
    +            return "unknown: event_type not provided"
    +
    +

    Instance variables

    +
    +
    var type : str
    +
    +
    +
    + +Expand source code + +
    @property
    +def type(self) -> str:
    +    try:
    +        return self.context['event_type']
    +    except KeyError:
    +        return "unknown: event_type not provided"
    +
    +
    +
    +
    +
    +class EventInterface +
    +
    +

    A dynamic interface for handling events.

    +

    This interface defines a single method, 'handle_event', which is intended +to be implemented by classes that want to handle specific types of events.

    +

    Use this interface as a mixin for classes (Proxy/Controller) that implement this handle_event method.

    +
    + +Expand source code + +
    class EventInterface:
    +    """
    +    A dynamic interface for handling events.
    +
    +    This interface defines a single method, 'handle_event', which is intended
    +    to be implemented by classes that want to handle specific types of events.
    +
    +    Use this interface as a mixin for classes (Proxy/Controller) that implement this `handle_event` method.
    +
    +    """
    +    @dynamic_interface
    +    def handle_event(self, event: Event):
    +        """Handles the specified event.
    +
    +        Args:
    +            event (Event): An instance of the Event class representing the event to be handled.
    +        """
    +        ...
    +
    +

    Subclasses

    + +

    Methods

    +
    +
    +def handle_event(self, event: Event) +
    +
    +

    Handles the specified event.

    +

    Args

    +
    +
    event : Event
    +
    An instance of the Event class representing the event to be handled.
    +
    +
    + +Expand source code + +
    @dynamic_interface
    +def handle_event(self, event: Event):
    +    """Handles the specified event.
    +
    +    Args:
    +        event (Event): An instance of the Event class representing the event to be handled.
    +    """
    +    ...
    +
    +
    +
    +
    +
    +class Listeners +
    +
    +

    A class for managing and notifying registered listeners.

    +

    This class provides methods to add, remove, and notify listeners of events.

    +

    Initializes an instance of the Listeners class.

    +
    + +Expand source code + +
    class Listeners:
    +    """
    +    A class for managing and notifying registered listeners.
    +
    +    This class provides methods to add, remove, and notify listeners of events.
    +    """
    +
    +    def __init__(self):
    +        """Initializes an instance of the Listeners class."""
    +        self._listeners: Dict[str, dict] = {}
    +
    +    def __len__(self):
    +        """Returns the number of registered listeners."""
    +        return len(self._listeners)
    +
    +    def add_listener(self, listener: dict):
    +        """
    +        Adds a new listener to the registered listeners.
    +
    +        The listener argument dictionary is expected to have at least the following key:values pairs:
    +
    +        * 'name': the name or identifier of the listener
    +        * 'proxy': a Proxy object that will be used for notifying the service
    +
    +        Args:
    +            listener (dict): A dictionary with properties of the listener,
    +                including 'name' and 'proxy'.
    +
    +        Raises:
    +            ValueError: If the listener already exists.
    +        """
    +        try:
    +            listener_name = listener["name"]
    +        except KeyError as exc:
    +            raise ValueError(f"Expected 'name' key in listener argument {listener}.") from exc
    +
    +        if listener_name in self._listeners:
    +            raise ValueError(f"Process {listener_name} is already registered as a listener.")
    +
    +        # For now we make these listeners have a Proxy class that will be used for notification. Later on
    +        # we might have other mechanisms for notification.
    +
    +        from egse.proxy import Proxy
    +
    +        proxy = listener.get('proxy')
    +        if proxy is not None:
    +            if not isinstance(proxy, type) or not issubclass(proxy, Proxy):
    +                raise ValueError(f"Expected 'proxy' in listener argument {proxy=} to be a Proxy sub-class.")
    +
    +        self._listeners[listener_name] = listener
    +
    +    def remove_listener(self, listener: dict):
    +        """
    +        Removes a listener from the registered listeners.
    +
    +        Args:
    +           listener (dict): A dictionary representing the listener to be
    +               removed. It should contain a 'name' key.
    +
    +        Raises:
    +           ValueError: If the 'name' key is not present in the listener
    +               argument or if the specified listener is not registered.
    +        """
    +
    +        try:
    +            listener_name = listener["name"]
    +        except KeyError as exc:
    +            raise ValueError(f"Expected 'name' key in listener argument {listener}.") from exc
    +
    +        try:
    +            del self._listeners[listener_name]
    +        except KeyError as exc:
    +            raise ValueError(f"Process {listener_name} cannot be removed, not registered.") from exc
    +
    +    def notify_listeners(self, event: Event):
    +        """
    +        Notifies all registered listeners of a specific event.
    +
    +        Args:
    +            event (Event): An instance of the Event class representing the
    +                event to be broadcasted.
    +
    +        Note:
    +            The 'handle_event' method is called on each listener's proxy
    +            object to process the event.
    +        """
    +
    +        LOGGER.debug(f"Notifying listeners {self.get_listener_names()}")
    +
    +        for name, listener in self._listeners.items():
    +            self.notify_listener(name, listener, event)
    +
    +    def notify_listener(self, name: str, listener: dict, event: Event):
    +        """
    +        Notifies a registered listener fro the given event.
    +        """
    +        proxy: Type[EventInterface] = listener['proxy']
    +
    +        # The Proxy uses a REQ-REP protocol and waits for an answer. That's why we have a Timer
    +        # in the context so we are able to monitor how long it takes to notify a listener.
    +        # The Timer can be removed when we are confident all `handle_event` functions are properly
    +        # implemented.
    +        with Timer(f"Notify listener {name}", log_level=logging.DEBUG), proxy() as pobj:
    +            rc = pobj.handle_event(event)
    +
    +        LOGGER.debug(f"Listener {name} returned {rc=}")
    +
    +    def get_listener_names(self) -> List[str]:
    +        """Returns a list with the names of the registered listeners."""
    +        return list(self._listeners.keys())
    +
    +

    Methods

    +
    +
    +def add_listener(self, listener: dict) +
    +
    +

    Adds a new listener to the registered listeners.

    +

    The listener argument dictionary is expected to have at least the following key:values pairs:

    +
      +
    • 'name': the name or identifier of the listener
    • +
    • 'proxy': a Proxy object that will be used for notifying the service
    • +
    +

    Args

    +
    +
    listener : dict
    +
    A dictionary with properties of the listener, +including 'name' and 'proxy'.
    +
    +

    Raises

    +
    +
    ValueError
    +
    If the listener already exists.
    +
    +
    + +Expand source code + +
    def add_listener(self, listener: dict):
    +    """
    +    Adds a new listener to the registered listeners.
    +
    +    The listener argument dictionary is expected to have at least the following key:values pairs:
    +
    +    * 'name': the name or identifier of the listener
    +    * 'proxy': a Proxy object that will be used for notifying the service
    +
    +    Args:
    +        listener (dict): A dictionary with properties of the listener,
    +            including 'name' and 'proxy'.
    +
    +    Raises:
    +        ValueError: If the listener already exists.
    +    """
    +    try:
    +        listener_name = listener["name"]
    +    except KeyError as exc:
    +        raise ValueError(f"Expected 'name' key in listener argument {listener}.") from exc
    +
    +    if listener_name in self._listeners:
    +        raise ValueError(f"Process {listener_name} is already registered as a listener.")
    +
    +    # For now we make these listeners have a Proxy class that will be used for notification. Later on
    +    # we might have other mechanisms for notification.
    +
    +    from egse.proxy import Proxy
    +
    +    proxy = listener.get('proxy')
    +    if proxy is not None:
    +        if not isinstance(proxy, type) or not issubclass(proxy, Proxy):
    +            raise ValueError(f"Expected 'proxy' in listener argument {proxy=} to be a Proxy sub-class.")
    +
    +    self._listeners[listener_name] = listener
    +
    +
    +
    +def get_listener_names(self) ‑> List[str] +
    +
    +

    Returns a list with the names of the registered listeners.

    +
    + +Expand source code + +
    def get_listener_names(self) -> List[str]:
    +    """Returns a list with the names of the registered listeners."""
    +    return list(self._listeners.keys())
    +
    +
    +
    +def notify_listener(self, name: str, listener: dict, event: Event) +
    +
    +

    Notifies a registered listener fro the given event.

    +
    + +Expand source code + +
    def notify_listener(self, name: str, listener: dict, event: Event):
    +    """
    +    Notifies a registered listener fro the given event.
    +    """
    +    proxy: Type[EventInterface] = listener['proxy']
    +
    +    # The Proxy uses a REQ-REP protocol and waits for an answer. That's why we have a Timer
    +    # in the context so we are able to monitor how long it takes to notify a listener.
    +    # The Timer can be removed when we are confident all `handle_event` functions are properly
    +    # implemented.
    +    with Timer(f"Notify listener {name}", log_level=logging.DEBUG), proxy() as pobj:
    +        rc = pobj.handle_event(event)
    +
    +    LOGGER.debug(f"Listener {name} returned {rc=}")
    +
    +
    +
    +def notify_listeners(self, event: Event) +
    +
    +

    Notifies all registered listeners of a specific event.

    +

    Args

    +
    +
    event : Event
    +
    An instance of the Event class representing the +event to be broadcasted.
    +
    +

    Note

    +

    The 'handle_event' method is called on each listener's proxy +object to process the event.

    +
    + +Expand source code + +
    def notify_listeners(self, event: Event):
    +    """
    +    Notifies all registered listeners of a specific event.
    +
    +    Args:
    +        event (Event): An instance of the Event class representing the
    +            event to be broadcasted.
    +
    +    Note:
    +        The 'handle_event' method is called on each listener's proxy
    +        object to process the event.
    +    """
    +
    +    LOGGER.debug(f"Notifying listeners {self.get_listener_names()}")
    +
    +    for name, listener in self._listeners.items():
    +        self.notify_listener(name, listener, event)
    +
    +
    +
    +def remove_listener(self, listener: dict) +
    +
    +

    Removes a listener from the registered listeners.

    +

    Args

    +
    +
    listener : dict
    +
    A dictionary representing the listener to be +removed. It should contain a 'name' key.
    +
    +

    Raises

    +
    +
    ValueError
    +
    If the 'name' key is not present in the listener +argument or if the specified listener is not registered.
    +
    +
    + +Expand source code + +
    def remove_listener(self, listener: dict):
    +    """
    +    Removes a listener from the registered listeners.
    +
    +    Args:
    +       listener (dict): A dictionary representing the listener to be
    +           removed. It should contain a 'name' key.
    +
    +    Raises:
    +       ValueError: If the 'name' key is not present in the listener
    +           argument or if the specified listener is not registered.
    +    """
    +
    +    try:
    +        listener_name = listener["name"]
    +    except KeyError as exc:
    +        raise ValueError(f"Expected 'name' key in listener argument {listener}.") from exc
    +
    +    try:
    +        del self._listeners[listener_name]
    +    except KeyError as exc:
    +        raise ValueError(f"Process {listener_name} cannot be removed, not registered.") from exc
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/api/egse/logger/index.html b/docs/api/egse/logger/index.html index ee4c700..4f06f2b 100644 --- a/docs/api/egse/logger/index.html +++ b/docs/api/egse/logger/index.html @@ -245,7 +245,7 @@

    Module egse.logger

    # handler.setLevel(level) -if "__main__" == __name__: +if __name__ == "__main__": import egse.logger diff --git a/docs/api/egse/metrics.html b/docs/api/egse/metrics.html index 175d770..5409118 100644 --- a/docs/api/egse/metrics.html +++ b/docs/api/egse/metrics.html @@ -27,20 +27,21 @@

    Module egse.metrics

    Expand source code
    import logging
    +from typing import Optional
     
     import numpy as np
     from prometheus_client import Gauge
     
     from egse.hk import TmDictionaryColumns
     from egse.settings import Settings
    -from egse.setup import SetupError
    +from egse.setup import SetupError, load_setup, Setup
     from egse.state import GlobalState
     
    -MODULE_LOGGER = logging.getLogger(__name__)
    +LOGGER = logging.getLogger(__name__)
     SITE_ID = Settings.load("SITE").ID
     
     
    -def define_metrics(origin: str, dashboard: str = None, use_site:bool = False) -> dict:
    +def define_metrics(origin: str, dashboard: str = None, use_site: bool = False, setup: Optional[Setup] = None) -> dict:
         """
         Create a metrics dictionary from the TM dictionary.
     
    @@ -53,14 +54,16 @@ 

    Module egse.metrics

    origin: the storage mnemonics for the requested metrics dashboard: restrict the metrics selection to those that are defined for the given dashboard. You can select all dashboards with `dashboard='*'`. - use_site: Indicate whether or not the prefixes of the new HK names are TH-specific + use_site: Indicate whether the prefixes of the new HK names are TH-specific + setup: Setup. Returns: Dictionary with all Prometheus gauges for the given origin and dashboard. """ - print("Defining metrics") + + setup = setup or load_setup() try: - hk_info_table = GlobalState.setup.telemetry.dictionary + hk_info_table = setup.telemetry.dictionary except AttributeError: raise SetupError("Version of the telemetry dictionary not specified in the current setup") @@ -84,27 +87,47 @@

    Module egse.metrics

    syn_names = hk_names[selection] descriptions = descriptions[selection] - if use_site: - - th_prefix = f"G{SITE_ID}_" - - th_syn_names = [] - th_descriptions = [] - for syn_name, description in zip(syn_names, descriptions): - if syn_name.startswith(th_prefix): - th_syn_names.append(syn_name) - th_descriptions.append(description) - + if not use_site: return { syn_name: Gauge(syn_name, description) - for syn_name, description in zip(th_syn_names, th_descriptions) + for syn_name, description in zip(syn_names, descriptions) } - else: - return { - syn_name: Gauge(syn_name, description) - for syn_name, description in zip(syn_names, descriptions) - }
    + th_prefix = f"G{SITE_ID}_" + + th_syn_names = [] + th_descriptions = [] + for syn_name, description in zip(syn_names, descriptions): + if syn_name.startswith(th_prefix): + th_syn_names.append(syn_name) + th_descriptions.append(description) + + return { + syn_name: Gauge(syn_name, description) + for syn_name, description in zip(th_syn_names, th_descriptions) + } + + +def update_metrics(metrics: dict, updates: dict): + """ + Updates the metrics parameters with the values from the updates dictionary. + Only the metrics parameters for which the names are keys in the given updates dict are actually updated. Other + metrics remain untouched. + + The functions logs a warning when the updates dict contains a name which is not known as a metrics parameter. + + Args: + metrics: the metrics dictionary previously defined with the define_metrics function + updates: a dictionary with key=metrics name and value is the to-be-updated value + """ + for metric_name, value in updates.items(): + try: + if value is None: + metrics[metric_name].set(float('nan')) + else: + metrics[metric_name].set(float(value)) + except KeyError as exc: + LOGGER.warning(f"Unknown metric name: {metric_name=}")
    @@ -115,7 +138,7 @@

    Module egse.metrics

    Functions

    -def define_metrics(origin: str, dashboard: str = None, use_site: bool = False) ‑> dict +def define_metrics(origin: str, dashboard: str = None, use_site: bool = False, setup: Optional[Setup] = None) ‑> dict

    Create a metrics dictionary from the TM dictionary.

    @@ -130,7 +153,9 @@

    Args

    restrict the metrics selection to those that are defined for the given dashboard. You can select all dashboards with dashboard='*'.
    use_site
    -
    Indicate whether or not the prefixes of the new HK names are TH-specific
    +
    Indicate whether the prefixes of the new HK names are TH-specific
    +
    setup
    +
    Setup.

    Returns

    Dictionary with all Prometheus gauges for the given origin and dashboard.

    @@ -138,7 +163,7 @@

    Returns

    Expand source code -
    def define_metrics(origin: str, dashboard: str = None, use_site:bool = False) -> dict:
    +
    def define_metrics(origin: str, dashboard: str = None, use_site: bool = False, setup: Optional[Setup] = None) -> dict:
         """
         Create a metrics dictionary from the TM dictionary.
     
    @@ -151,14 +176,16 @@ 

    Returns

    origin: the storage mnemonics for the requested metrics dashboard: restrict the metrics selection to those that are defined for the given dashboard. You can select all dashboards with `dashboard='*'`. - use_site: Indicate whether or not the prefixes of the new HK names are TH-specific + use_site: Indicate whether the prefixes of the new HK names are TH-specific + setup: Setup. Returns: Dictionary with all Prometheus gauges for the given origin and dashboard. """ - print("Defining metrics") + + setup = setup or load_setup() try: - hk_info_table = GlobalState.setup.telemetry.dictionary + hk_info_table = setup.telemetry.dictionary except AttributeError: raise SetupError("Version of the telemetry dictionary not specified in the current setup") @@ -182,27 +209,66 @@

    Returns

    syn_names = hk_names[selection] descriptions = descriptions[selection] - if use_site: - - th_prefix = f"G{SITE_ID}_" - - th_syn_names = [] - th_descriptions = [] - for syn_name, description in zip(syn_names, descriptions): - if syn_name.startswith(th_prefix): - th_syn_names.append(syn_name) - th_descriptions.append(description) - + if not use_site: return { syn_name: Gauge(syn_name, description) - for syn_name, description in zip(th_syn_names, th_descriptions) + for syn_name, description in zip(syn_names, descriptions) } - else: - return { - syn_name: Gauge(syn_name, description) - for syn_name, description in zip(syn_names, descriptions) - }
    + th_prefix = f"G{SITE_ID}_" + + th_syn_names = [] + th_descriptions = [] + for syn_name, description in zip(syn_names, descriptions): + if syn_name.startswith(th_prefix): + th_syn_names.append(syn_name) + th_descriptions.append(description) + + return { + syn_name: Gauge(syn_name, description) + for syn_name, description in zip(th_syn_names, th_descriptions) + }
    + + +
    +def update_metrics(metrics: dict, updates: dict) +
    +
    +

    Updates the metrics parameters with the values from the updates dictionary. +Only the metrics parameters for which the names are keys in the given updates dict are actually updated. Other +metrics remain untouched.

    +

    The functions logs a warning when the updates dict contains a name which is not known as a metrics parameter.

    +

    Args

    +
    +
    metrics
    +
    the metrics dictionary previously defined with the define_metrics function
    +
    updates
    +
    a dictionary with key=metrics name and value is the to-be-updated value
    +
    +
    + +Expand source code + +
    def update_metrics(metrics: dict, updates: dict):
    +    """
    +    Updates the metrics parameters with the values from the updates dictionary.
    +    Only the metrics parameters for which the names are keys in the given updates dict are actually updated. Other
    +    metrics remain untouched.
    +
    +    The functions logs a warning when the updates dict contains a name which is not known as a metrics parameter.
    +
    +    Args:
    +        metrics: the metrics dictionary previously defined with the define_metrics function
    +        updates: a dictionary with key=metrics name and value is the to-be-updated value
    +    """
    +    for metric_name, value in updates.items():
    +        try:
    +            if value is None:
    +                metrics[metric_name].set(float('nan'))
    +            else:
    +                metrics[metric_name].set(float(value))
    +        except KeyError as exc:
    +            LOGGER.warning(f"Unknown metric name: {metric_name=}")
    @@ -224,6 +290,7 @@

    Index

  • Functions

  • diff --git a/docs/api/egse/monitoring.html b/docs/api/egse/monitoring.html index 6771fc5..a3f868e 100644 --- a/docs/api/egse/monitoring.html +++ b/docs/api/egse/monitoring.html @@ -36,15 +36,11 @@

    Module egse.monitoring

    from egse.control import ControlServer from egse.protocol import CommandProtocol -from egse.settings import Settings -from egse.storage.persistence import CSV from egse.system import format_datetime from egse.zmq import MessageIdentifier from egse.zmq_ser import bind_address -logging.basicConfig(level=logging.INFO, format= Settings.LOG_FORMAT_FULL) -logger = logging.getLogger("monitoring") - +logger = logging.getLogger("egse.monitoring") class MonitoringProtocol(CommandProtocol): def __init__(self, control_server: ControlServer): @@ -68,17 +64,15 @@

    Module egse.monitoring

    } -@click.group() -def cli(): - pass - -@cli.command() +@click.command() @click.argument('hostname') @click.argument('port') -@click.option('--subscribe', '-s', default='ALL', help="subscribe to a sync identifier") +@click.option('--subscribe', '-s', default=('ALL',), multiple=True, + help="subscribe to a sync identifier, can appear multiple times") @click.option('--multipart', '-m', is_flag=True, default=False, help="use multipart messages") -def monitoring(hostname: str, port: int, subscribe: str, multipart: bool): +@click.option('--pickle/--no-pickle', 'use_pickle', default=True) +def monitoring(hostname: str, port: int, subscribe: str, multipart: bool, use_pickle: bool): """Monitor the status of a control server on hostname:port. The port number shall correspond to the port number on which the control server is publishing @@ -89,44 +83,44 @@

    Module egse.monitoring

    receiver = context.socket(zmq.SUB) receiver.connect(f"tcp://{hostname}:{port}") - sync_id = 0 - try: - sync_id = MessageIdentifier[subscribe.upper()] - except KeyError: - rich.print(f"[red]ERROR: incorrect subscribe identifier, " - f"use one of {[x.name for x in MessageIdentifier]}") - ctx = click.get_current_context() - rich.print(ctx.get_help()) - ctx.exit() - - if sync_id == MessageIdentifier.ALL: - subscribe_string = b'' - else: - subscribe_string = sync_id.to_bytes(1, byteorder='big') - - receiver.subscribe(subscribe_string) - - with CSV(filename=f"monitoring-{format_datetime()}-{port}.csv", - prep={"column_names": ["timestamp", "Up", "RSS", "USS"], "mode": 'w'}) as csv: - while True: - try: - if multipart: - sync_id, pickle_string = receiver.recv_multipart() - sync_id = int.from_bytes(sync_id, byteorder='big') - else: - sync_id = MessageIdentifier.ALL - pickle_string = receiver.recv() - status = pickle.loads(pickle_string) - logger.debug(f"{MessageIdentifier(sync_id).name}, {status}") - csv.create(status) - except KeyboardInterrupt: - logger.info("KeyboardInterrupt caught!") - break - + for item in subscribe: + sync_id = 0 + try: + sync_id = MessageIdentifier[item.upper()] + except KeyError: + rich.print(f"[red]ERROR: incorrect subscribe identifier, " + f"use one of {[x.name for x in MessageIdentifier]}") + ctx = click.get_current_context() + rich.print(ctx.get_help()) + ctx.exit() + + if sync_id == MessageIdentifier.ALL: + subscribe_string = b'' + else: + subscribe_string = sync_id.to_bytes(1, byteorder='big') + + receiver.subscribe(subscribe_string) + + while True: + try: + if multipart: + sync_id, message = receiver.recv_multipart() + sync_id = int.from_bytes(sync_id, byteorder='big') + else: + sync_id = MessageIdentifier.ALL + message = receiver.recv() + response = pickle.loads(message) if use_pickle else message + rich.print(f"{MessageIdentifier(sync_id).name}, {response}", flush=True) + except KeyboardInterrupt: + logger.info("KeyboardInterrupt caught!") + break + + receiver.close(linger=0) + context.term() if __name__ == "__main__": multiprocessing.current_process().name = "Monitoring" - cli()
    + monitoring()
    diff --git a/docs/api/egse/ni/alarms/cdaq9375.html b/docs/api/egse/ni/alarms/cdaq9375.html new file mode 100644 index 0000000..cefa92d --- /dev/null +++ b/docs/api/egse/ni/alarms/cdaq9375.html @@ -0,0 +1,1039 @@ + + + + + + +egse.ni.alarms.cdaq9375 API documentation + + + + + + + + + + + +
    +
    +
    +

    Module egse.ni.alarms.cdaq9375

    +
    +
    +

    This module defines the basic classes to access the UPS and TVAC info and send alarms to TVAC readout ensured by +NI CDAQ9375 controller that is used.

    +
    + +Expand source code + +
    """
    +This module defines the basic classes to access the UPS and TVAC info and send alarms to TVAC readout ensured by
    +NI CDAQ9375 controller that is used.
    +"""
    +import logging
    +from typing import List
    +
    +from egse.decorators import dynamic_interface
    +from egse.device import DeviceConnectionState
    +from egse.device import DeviceInterface
    +from egse.ni.alarms.cdaq9375_devif import cdaq9375Error, cdaq9375SocketInterface
    +from egse.proxy import Proxy
    +from egse.settings import Settings
    +from egse.zmq_ser import connect_address
    +from egse.mixin import dynamic_command
    +from egse.mixin import add_lf
    +from egse.system import format_datetime
    +from datetime import datetime
    +from numpy import array, sum
    +
    +
    +LOGGER = logging.getLogger(__name__)
    +
    +CTRL_SETTINGS = Settings.load("NI Control Server")
    +CDAQ_SETTINGS = Settings.load("NI Controller")
    +DEVICE_SETTINGS = Settings.load(filename="cdaq9375.yaml")
    +
    +
    +class cdaq9375Interface(DeviceInterface):
    +    """
    +    Interface definition for the Signals acquisitions and emission of the CDAQ9375 Controller, Simulator and Proxy..
    +    """
    +    @dynamic_interface
    +    def info(self) -> str:
    +        """
    +        Returns basic information about the device, its name, firmware version etc.
    +
    +        The string returned is subject to change without notice and can not be used for parsing
    +        information.
    +
    +        Returns:
    +            An identification string of the instrument.
    +        """
    +        raise NotImplementedError
    +
    +
    +    @dynamic_interface
    +    def get_tvac_and_ups_state(self):
    +        """
    +        Returns a dictionary with the state (1 or 0) of UPS_Ix and UPS_Arrakis (alarm summary and power supply absence)
    +        and the state of TVAC. The timestamp (well formatted with the fucntion format_datetime) with the CDAQ9375 time
    +        is included in the dictionary:
    +        {"timestamp": 2023-05-17T13:27:45.335,
    +        "UPS_Arrakis_alarm_summary": 1/0,
    +        "UPS_Arrakis_power_supply_absence": 1/0,
    +        "UPS_Ix_alarm_summary": 1/0,
    +        "UPS_Ix_power_supply_absence": 1/0,
    +        "TVAC_low_temp": 1/0,
    +        "TVAC_High_Temp": 1/0,
    +        "TVAC_Interlock_Cryo": 1/0,
    +        "TVAC_vacuum": 1/0}
    +        """
    +
    +        raise NotImplementedError
    +
    +
    +    @dynamic_interface
    +    def send_egse_state_to_tvac(self):
    +        """
    +        Send to TVAC the state of EGSE (EXP1, EXP2, EXP3, EXP4)
    +        EXP1: Low Temp NOP
    +        EXP2: High Temp NOP
    +        EXP3: low/High Temp OP
    +        EXP4: UPS alarm (UPS_Ix_alarm_summary or UPS_Ix_power_supply_absence or UPS_Arrakis_alarm_summary or UPS_Arrakis_power_supply_absence)
    +        """
    +
    +        raise NotImplementedError
    +
    +    @dynamic_interface
    +    def dec2bitlist_compl(self, d:int):
    +        """
    +        Decompose a integer (base 10) into a list of power of 2.
    +        e.g. 14 -> [8, 4, 2] (8+4+2 = 14)
    +        Args:
    +            d: integer base 10 to decompose (d <= 15)
    +        returns:
    +            list of power of 2
    +        """
    +
    +        raise  NotImplementedError
    +
    +
    +
    +class cdaq9375Simulator(cdaq9375Interface):
    +    """
    +    The cdaq9375 Simulator class.
    +    """
    +
    +    def __init__(self):
    +        self._is_connected = True
    +        self.temp_operation = False
    +
    +    def is_connected(self):
    +        return self._is_connected
    +
    +    def is_simulator(self):
    +        return True
    +
    +    def connect(self):
    +        self._is_connected = True
    +
    +    def disconnect(self):
    +        self._is_connected = False
    +
    +    def reconnect(self):
    +        self.connect()
    +
    +    def get_id(self):
    +        pass
    +
    +    def info(self):
    +        pass
    +
    +    def get_tvac_and_ups_state(self):
    +        pass
    +
    +    def send_egse_state_to_tvac(self):
    +        pass
    +
    +    def dec2bitlist_compl(self, d:int):
    +        pass
    +
    +
    +class cdaq9375Controller(cdaq9375Interface):
    +    """The cdaq9375Controller allows controlling the NI CDAQ 9375 alarms device."""
    +
    +    def __init__(self):
    +        """Initialize the cdaq9375 Controller interface."""
    +
    +        super().__init__()
    +
    +        LOGGER.debug("Initializing NI CDAQ 9375")
    +
    +        try:
    +            self.cdaq = cdaq9375SocketInterface()
    +            self.cdaq.connect(CDAQ_SETTINGS.HOSTNAME) # IP address of the computer with Labview
    +
    +        except cdaq9375Error as exc:
    +            LOGGER.warning(f"TempError caught: Couldn't establish connection ({exc})")
    +            raise cdaq9375Error(
    +                "Couldn't establish a connection with the NI CDAQ 9375 controller."
    +            ) from exc
    +
    +    def connect(self):
    +        """Connects to the CDAQ9375 alarms device.
    +
    +        Raises:
    +            DeviceNotFoundError: when the CDAQ9375 alarms device is not connected.
    +        """
    +        try:
    +            self.cdaq.connect(CDAQ_SETTINGS.HOSTNAME)
    +        except cdaq9Error as exc:
    +             LOGGER.warning(f"cdaqError caught: Couldn't establish connection ({exc})")
    +             raise ConnectionError("Couldn't establish a connection with the NI DAQ9375 controller.") from exc
    +
    +        self.notify_observers(DeviceConnectionState.DEVICE_CONNECTED)
    +
    +
    +    def disconnect(self):
    +        try:
    +            self.cdaq.disconnect()
    +        except cdaq9375Error as exc:
    +            raise ConnectionError("Couldn't establish a connection with the NI CDAQ 9375 controller.") from exc
    +
    +        self.notify_observers(DeviceConnectionState.DEVICE_NOT_CONNECTED)
    +
    +
    +    def reconnect(self):
    +        if self.is_connected():
    +            self.disconnect()
    +        self.connect()
    +
    +    def is_connected(self):
    +        """Check if the CDAQ9375 Controller is connected."""
    +        return self.cdaq.is_connected()
    +
    +    def is_simulator(self):
    +        return False
    +
    +    def get_id(self):
    +        return self.cdaq.get_id()
    +
    +    def info(self):
    +        pass
    +
    +    def get_tvac_and_ups_state(self) -> dict:
    +        """
    +        Returns a dictionary with the state (1 or 0) of UPS_Ix and UPS_Arrakis (alarm summary and power supply absence)
    +        and the state of TVAC. The timestamp (well formatted with the fucntion format_datetime) with the CDAQ9375 time
    +        is included in the dictionary:
    +        {"timestamp": 2023-05-17T13:27:45.335,
    +        "UPS_Arrakis_alarm_summary": 1/0,
    +        "UPS_Arrakis_power_supply_absence": 1/0,
    +        "UPS_Ix_alarm_summary": 1/0,
    +        "UPS_Ix_power_supply_absence": 1/0,
    +        "TVAC_low_temp": 1/0,
    +        "TVAC_High_Temp": 1/0,
    +        "TVAC_Interlock_Cryo": 1/0,
    +        "TVAC_vacuum": 1/0}
    +        """
    +
    +        response = self.cdaq.trans("\x00\x00\x00\x16READ_P1P2_NIDAQ_9181")
    +        # the 2 last elements of response are the alarm values, first is P2 (UPS) and second is P1 (TVAC): P2P1
    +        # Contacts both for P1 and P2 are normally closed, so we must take the complement
    +
    +        response = response.strip()
    +        data_time = response.split("R")[0]
    +        timestamp = format_datetime(datetime.strptime(data_time, "%y/%m/%d%H:%M:%S.%f"))
    +
    +        total_alarm = {"timestamp": timestamp}
    +
    +        p2_code = int(response[-2], 16)
    +        p2_bitlist = self.dec2bitlist_compl(p2_code)
    +        p2_bit_to_alarm_name = {1: "UPS_Arrakis_alarm_summary", 2: "UPS_Arrakis_power_supply_absence", 4: "UPS_Ix_alarm_summary", 8: "UPS_Ix_power_supply_absence"}
    +        p2_alarm = {"UPS_Arrakis_alarm_summary": 0, "UPS_Arrakis_power_supply_absence": 0, "UPS_Ix_alarm_summary": 0, "UPS_Ix_power_supply_absence": 0}
    +        if p2_bitlist != []:
    +            for bit in p2_bitlist:
    +                p2_alarm[p2_bit_to_alarm_name[bit]] = 1
    +
    +        p1_code = int(response[-1], 16)
    +        p1_bitlist = self.dec2bitlist_compl(p1_code)
    +        p1_bit_to_alarm_name = {1: "TVAC_low_temp", 2: "TVAC_High_Temp", 4: "TVAC_Interlock_Cryo", 8: "TVAC_vacuum"}
    +        p1_alarm = {"TVAC_low_temp": 0, "TVAC_High_Temp": 0, "TVAC_Interlock_Cryo": 0, "TVAC_vacuum": 0}
    +        if p1_bitlist != []:
    +            for bit in p1_bitlist:
    +                p1_alarm[p1_bit_to_alarm_name[bit]] = 1
    +
    +        total_alarm.update(p2_alarm)
    +        total_alarm.update(p1_alarm)
    +
    +        return total_alarm
    +
    +    def send_egse_state_to_tvac(self, alarm_exp1: bool, alarm_exp2: bool, alarm_exp3: bool, alarm_exp4: bool) -> str:
    +        """
    +        Send to TVAC the state of EGSE (EXP1, EXP2, EXP3, EXP4)
    +        EXP1: Low Temp NOP
    +        EXP2: High Temp NOP
    +        EXP3: low/High Temp OP
    +        EXP4: UPS alarm (UPS_Ix_alarm_summary or UPS_Ix_power_supply_absence or UPS_Arrakis_alarm_summary or UPS_Arrakis_power_supply_absence)
    +        """
    +        alarm_list = [int(not(alarm_exp1)), int(not(alarm_exp2)), int(not(alarm_exp3)), int(not(alarm_exp4))]
    +        bit_array = array([alarm_list[i] * 2 ** i for i in range(4)])
    +        bit_array = bit_array[bit_array != 0]
    +        if len(bit_array) == 0:
    +            return self.cdaq.trans("\x00\x00\x00\x16WRITE_P3X_NIDAQ_918100")
    +        else:
    +            value_alarm = hex(sum(bit_array))[-1].upper()
    +            return self.cdaq.trans(f"\x00\x00\x00\x16WRITE_P3X_NIDAQ_91810{value_alarm}")
    +
    +
    +    def dec2bitlist_compl(self, d:int) -> list:
    +        """
    +        Decompose a integer (base 10) into a list of power of 2.
    +        e.g. 14 -> [8, 4, 2] (8+4+2 = 14)
    +        Args:
    +            d: integer base 10 to decompose (d <= 15)
    +        returns:
    +            list of power of 2
    +        """
    +        bits = []
    +        bits_compl = []
    +        for i in [8, 4, 2, 1]:
    +            if i <= d:
    +                bits.append(i)
    +                d %= i
    +        for i in [8, 4, 2, 1]:
    +            if i not in bits:
    +                bits_compl.append(i)
    +        return bits_compl
    +
    +class cdaq9375Proxy(Proxy, cdaq9375Interface):
    +    """The cdaq9375Proxy class is used to connect to the control server and send commands to
    +    the NI CDAQ 9375 device remotely."""
    +    def __init__(
    +        self,
    +        protocol=CTRL_SETTINGS.PROTOCOL,
    +        hostname=CTRL_SETTINGS.HOSTNAME,
    +        port=CTRL_SETTINGS.CDAQ9375.get("COMMANDING_PORT"),
    +        timeout = CTRL_SETTINGS.CDAQ9375.get("TIMEOUT") * 1000
    +    ):
    +        """
    +        Args:
    +            protocol: the transport protocol [default is taken from settings file]
    +            hostname: location of the control server (IP address)
    +                [default is taken from settings file]
    +            port: TCP port on which the control server is listening for commands
    +                [default is taken from settings file]
    +        """
    +        super().__init__(connect_address(protocol, hostname, port), timeout=timeout)
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class cdaq9375Controller +
    +
    +

    The cdaq9375Controller allows controlling the NI CDAQ 9375 alarms device.

    +

    Initialize the cdaq9375 Controller interface.

    +
    + +Expand source code + +
    class cdaq9375Controller(cdaq9375Interface):
    +    """The cdaq9375Controller allows controlling the NI CDAQ 9375 alarms device."""
    +
    +    def __init__(self):
    +        """Initialize the cdaq9375 Controller interface."""
    +
    +        super().__init__()
    +
    +        LOGGER.debug("Initializing NI CDAQ 9375")
    +
    +        try:
    +            self.cdaq = cdaq9375SocketInterface()
    +            self.cdaq.connect(CDAQ_SETTINGS.HOSTNAME) # IP address of the computer with Labview
    +
    +        except cdaq9375Error as exc:
    +            LOGGER.warning(f"TempError caught: Couldn't establish connection ({exc})")
    +            raise cdaq9375Error(
    +                "Couldn't establish a connection with the NI CDAQ 9375 controller."
    +            ) from exc
    +
    +    def connect(self):
    +        """Connects to the CDAQ9375 alarms device.
    +
    +        Raises:
    +            DeviceNotFoundError: when the CDAQ9375 alarms device is not connected.
    +        """
    +        try:
    +            self.cdaq.connect(CDAQ_SETTINGS.HOSTNAME)
    +        except cdaq9Error as exc:
    +             LOGGER.warning(f"cdaqError caught: Couldn't establish connection ({exc})")
    +             raise ConnectionError("Couldn't establish a connection with the NI DAQ9375 controller.") from exc
    +
    +        self.notify_observers(DeviceConnectionState.DEVICE_CONNECTED)
    +
    +
    +    def disconnect(self):
    +        try:
    +            self.cdaq.disconnect()
    +        except cdaq9375Error as exc:
    +            raise ConnectionError("Couldn't establish a connection with the NI CDAQ 9375 controller.") from exc
    +
    +        self.notify_observers(DeviceConnectionState.DEVICE_NOT_CONNECTED)
    +
    +
    +    def reconnect(self):
    +        if self.is_connected():
    +            self.disconnect()
    +        self.connect()
    +
    +    def is_connected(self):
    +        """Check if the CDAQ9375 Controller is connected."""
    +        return self.cdaq.is_connected()
    +
    +    def is_simulator(self):
    +        return False
    +
    +    def get_id(self):
    +        return self.cdaq.get_id()
    +
    +    def info(self):
    +        pass
    +
    +    def get_tvac_and_ups_state(self) -> dict:
    +        """
    +        Returns a dictionary with the state (1 or 0) of UPS_Ix and UPS_Arrakis (alarm summary and power supply absence)
    +        and the state of TVAC. The timestamp (well formatted with the fucntion format_datetime) with the CDAQ9375 time
    +        is included in the dictionary:
    +        {"timestamp": 2023-05-17T13:27:45.335,
    +        "UPS_Arrakis_alarm_summary": 1/0,
    +        "UPS_Arrakis_power_supply_absence": 1/0,
    +        "UPS_Ix_alarm_summary": 1/0,
    +        "UPS_Ix_power_supply_absence": 1/0,
    +        "TVAC_low_temp": 1/0,
    +        "TVAC_High_Temp": 1/0,
    +        "TVAC_Interlock_Cryo": 1/0,
    +        "TVAC_vacuum": 1/0}
    +        """
    +
    +        response = self.cdaq.trans("\x00\x00\x00\x16READ_P1P2_NIDAQ_9181")
    +        # the 2 last elements of response are the alarm values, first is P2 (UPS) and second is P1 (TVAC): P2P1
    +        # Contacts both for P1 and P2 are normally closed, so we must take the complement
    +
    +        response = response.strip()
    +        data_time = response.split("R")[0]
    +        timestamp = format_datetime(datetime.strptime(data_time, "%y/%m/%d%H:%M:%S.%f"))
    +
    +        total_alarm = {"timestamp": timestamp}
    +
    +        p2_code = int(response[-2], 16)
    +        p2_bitlist = self.dec2bitlist_compl(p2_code)
    +        p2_bit_to_alarm_name = {1: "UPS_Arrakis_alarm_summary", 2: "UPS_Arrakis_power_supply_absence", 4: "UPS_Ix_alarm_summary", 8: "UPS_Ix_power_supply_absence"}
    +        p2_alarm = {"UPS_Arrakis_alarm_summary": 0, "UPS_Arrakis_power_supply_absence": 0, "UPS_Ix_alarm_summary": 0, "UPS_Ix_power_supply_absence": 0}
    +        if p2_bitlist != []:
    +            for bit in p2_bitlist:
    +                p2_alarm[p2_bit_to_alarm_name[bit]] = 1
    +
    +        p1_code = int(response[-1], 16)
    +        p1_bitlist = self.dec2bitlist_compl(p1_code)
    +        p1_bit_to_alarm_name = {1: "TVAC_low_temp", 2: "TVAC_High_Temp", 4: "TVAC_Interlock_Cryo", 8: "TVAC_vacuum"}
    +        p1_alarm = {"TVAC_low_temp": 0, "TVAC_High_Temp": 0, "TVAC_Interlock_Cryo": 0, "TVAC_vacuum": 0}
    +        if p1_bitlist != []:
    +            for bit in p1_bitlist:
    +                p1_alarm[p1_bit_to_alarm_name[bit]] = 1
    +
    +        total_alarm.update(p2_alarm)
    +        total_alarm.update(p1_alarm)
    +
    +        return total_alarm
    +
    +    def send_egse_state_to_tvac(self, alarm_exp1: bool, alarm_exp2: bool, alarm_exp3: bool, alarm_exp4: bool) -> str:
    +        """
    +        Send to TVAC the state of EGSE (EXP1, EXP2, EXP3, EXP4)
    +        EXP1: Low Temp NOP
    +        EXP2: High Temp NOP
    +        EXP3: low/High Temp OP
    +        EXP4: UPS alarm (UPS_Ix_alarm_summary or UPS_Ix_power_supply_absence or UPS_Arrakis_alarm_summary or UPS_Arrakis_power_supply_absence)
    +        """
    +        alarm_list = [int(not(alarm_exp1)), int(not(alarm_exp2)), int(not(alarm_exp3)), int(not(alarm_exp4))]
    +        bit_array = array([alarm_list[i] * 2 ** i for i in range(4)])
    +        bit_array = bit_array[bit_array != 0]
    +        if len(bit_array) == 0:
    +            return self.cdaq.trans("\x00\x00\x00\x16WRITE_P3X_NIDAQ_918100")
    +        else:
    +            value_alarm = hex(sum(bit_array))[-1].upper()
    +            return self.cdaq.trans(f"\x00\x00\x00\x16WRITE_P3X_NIDAQ_91810{value_alarm}")
    +
    +
    +    def dec2bitlist_compl(self, d:int) -> list:
    +        """
    +        Decompose a integer (base 10) into a list of power of 2.
    +        e.g. 14 -> [8, 4, 2] (8+4+2 = 14)
    +        Args:
    +            d: integer base 10 to decompose (d <= 15)
    +        returns:
    +            list of power of 2
    +        """
    +        bits = []
    +        bits_compl = []
    +        for i in [8, 4, 2, 1]:
    +            if i <= d:
    +                bits.append(i)
    +                d %= i
    +        for i in [8, 4, 2, 1]:
    +            if i not in bits:
    +                bits_compl.append(i)
    +        return bits_compl
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +def connect(self) +
    +
    +

    Connects to the CDAQ9375 alarms device.

    +

    Raises

    +
    +
    DeviceNotFoundError
    +
    when the CDAQ9375 alarms device is not connected.
    +
    +
    + +Expand source code + +
    def connect(self):
    +    """Connects to the CDAQ9375 alarms device.
    +
    +    Raises:
    +        DeviceNotFoundError: when the CDAQ9375 alarms device is not connected.
    +    """
    +    try:
    +        self.cdaq.connect(CDAQ_SETTINGS.HOSTNAME)
    +    except cdaq9Error as exc:
    +         LOGGER.warning(f"cdaqError caught: Couldn't establish connection ({exc})")
    +         raise ConnectionError("Couldn't establish a connection with the NI DAQ9375 controller.") from exc
    +
    +    self.notify_observers(DeviceConnectionState.DEVICE_CONNECTED)
    +
    +
    +
    +def get_id(self) +
    +
    +
    +
    + +Expand source code + +
    def get_id(self):
    +    return self.cdaq.get_id()
    +
    +
    +
    +def is_connected(self) +
    +
    +

    Check if the CDAQ9375 Controller is connected.

    +
    + +Expand source code + +
    def is_connected(self):
    +    """Check if the CDAQ9375 Controller is connected."""
    +    return self.cdaq.is_connected()
    +
    +
    +
    +

    Inherited members

    + +
    +
    +class cdaq9375Interface +
    +
    +

    Interface definition for the Signals acquisitions and emission of the CDAQ9375 Controller, Simulator and Proxy..

    +
    + +Expand source code + +
    class cdaq9375Interface(DeviceInterface):
    +    """
    +    Interface definition for the Signals acquisitions and emission of the CDAQ9375 Controller, Simulator and Proxy..
    +    """
    +    @dynamic_interface
    +    def info(self) -> str:
    +        """
    +        Returns basic information about the device, its name, firmware version etc.
    +
    +        The string returned is subject to change without notice and can not be used for parsing
    +        information.
    +
    +        Returns:
    +            An identification string of the instrument.
    +        """
    +        raise NotImplementedError
    +
    +
    +    @dynamic_interface
    +    def get_tvac_and_ups_state(self):
    +        """
    +        Returns a dictionary with the state (1 or 0) of UPS_Ix and UPS_Arrakis (alarm summary and power supply absence)
    +        and the state of TVAC. The timestamp (well formatted with the fucntion format_datetime) with the CDAQ9375 time
    +        is included in the dictionary:
    +        {"timestamp": 2023-05-17T13:27:45.335,
    +        "UPS_Arrakis_alarm_summary": 1/0,
    +        "UPS_Arrakis_power_supply_absence": 1/0,
    +        "UPS_Ix_alarm_summary": 1/0,
    +        "UPS_Ix_power_supply_absence": 1/0,
    +        "TVAC_low_temp": 1/0,
    +        "TVAC_High_Temp": 1/0,
    +        "TVAC_Interlock_Cryo": 1/0,
    +        "TVAC_vacuum": 1/0}
    +        """
    +
    +        raise NotImplementedError
    +
    +
    +    @dynamic_interface
    +    def send_egse_state_to_tvac(self):
    +        """
    +        Send to TVAC the state of EGSE (EXP1, EXP2, EXP3, EXP4)
    +        EXP1: Low Temp NOP
    +        EXP2: High Temp NOP
    +        EXP3: low/High Temp OP
    +        EXP4: UPS alarm (UPS_Ix_alarm_summary or UPS_Ix_power_supply_absence or UPS_Arrakis_alarm_summary or UPS_Arrakis_power_supply_absence)
    +        """
    +
    +        raise NotImplementedError
    +
    +    @dynamic_interface
    +    def dec2bitlist_compl(self, d:int):
    +        """
    +        Decompose a integer (base 10) into a list of power of 2.
    +        e.g. 14 -> [8, 4, 2] (8+4+2 = 14)
    +        Args:
    +            d: integer base 10 to decompose (d <= 15)
    +        returns:
    +            list of power of 2
    +        """
    +
    +        raise  NotImplementedError
    +
    +

    Ancestors

    + +

    Subclasses

    + +

    Methods

    +
    +
    +def dec2bitlist_compl(self, d: int) +
    +
    +

    Decompose a integer (base 10) into a list of power of 2. +e.g. 14 -> [8, 4, 2] (8+4+2 = 14)

    +

    Args

    +
    +
    d
    +
    integer base 10 to decompose (d <= 15)
    +
    +

    returns: +list of power of 2

    +
    + +Expand source code + +
    @dynamic_interface
    +def dec2bitlist_compl(self, d:int):
    +    """
    +    Decompose a integer (base 10) into a list of power of 2.
    +    e.g. 14 -> [8, 4, 2] (8+4+2 = 14)
    +    Args:
    +        d: integer base 10 to decompose (d <= 15)
    +    returns:
    +        list of power of 2
    +    """
    +
    +    raise  NotImplementedError
    +
    +
    +
    +def get_tvac_and_ups_state(self) +
    +
    +

    Returns a dictionary with the state (1 or 0) of UPS_Ix and UPS_Arrakis (alarm summary and power supply absence) +and the state of TVAC. The timestamp (well formatted with the fucntion format_datetime) with the CDAQ9375 time +is included in the dictionary: +{"timestamp": 2023-05-17T13:27:45.335, +"UPS_Arrakis_alarm_summary": 1/0, +"UPS_Arrakis_power_supply_absence": 1/0, +"UPS_Ix_alarm_summary": 1/0, +"UPS_Ix_power_supply_absence": 1/0, +"TVAC_low_temp": 1/0, +"TVAC_High_Temp": 1/0, +"TVAC_Interlock_Cryo": 1/0, +"TVAC_vacuum": 1/0}

    +
    + +Expand source code + +
    @dynamic_interface
    +def get_tvac_and_ups_state(self):
    +    """
    +    Returns a dictionary with the state (1 or 0) of UPS_Ix and UPS_Arrakis (alarm summary and power supply absence)
    +    and the state of TVAC. The timestamp (well formatted with the fucntion format_datetime) with the CDAQ9375 time
    +    is included in the dictionary:
    +    {"timestamp": 2023-05-17T13:27:45.335,
    +    "UPS_Arrakis_alarm_summary": 1/0,
    +    "UPS_Arrakis_power_supply_absence": 1/0,
    +    "UPS_Ix_alarm_summary": 1/0,
    +    "UPS_Ix_power_supply_absence": 1/0,
    +    "TVAC_low_temp": 1/0,
    +    "TVAC_High_Temp": 1/0,
    +    "TVAC_Interlock_Cryo": 1/0,
    +    "TVAC_vacuum": 1/0}
    +    """
    +
    +    raise NotImplementedError
    +
    +
    +
    +def info(self) ‑> str +
    +
    +

    Returns basic information about the device, its name, firmware version etc.

    +

    The string returned is subject to change without notice and can not be used for parsing +information.

    +

    Returns

    +

    An identification string of the instrument.

    +
    + +Expand source code + +
    @dynamic_interface
    +def info(self) -> str:
    +    """
    +    Returns basic information about the device, its name, firmware version etc.
    +
    +    The string returned is subject to change without notice and can not be used for parsing
    +    information.
    +
    +    Returns:
    +        An identification string of the instrument.
    +    """
    +    raise NotImplementedError
    +
    +
    +
    +def send_egse_state_to_tvac(self) +
    +
    +

    Send to TVAC the state of EGSE (EXP1, EXP2, EXP3, EXP4) +EXP1: Low Temp NOP +EXP2: High Temp NOP +EXP3: low/High Temp OP +EXP4: UPS alarm (UPS_Ix_alarm_summary or UPS_Ix_power_supply_absence or UPS_Arrakis_alarm_summary or UPS_Arrakis_power_supply_absence)

    +
    + +Expand source code + +
    @dynamic_interface
    +def send_egse_state_to_tvac(self):
    +    """
    +    Send to TVAC the state of EGSE (EXP1, EXP2, EXP3, EXP4)
    +    EXP1: Low Temp NOP
    +    EXP2: High Temp NOP
    +    EXP3: low/High Temp OP
    +    EXP4: UPS alarm (UPS_Ix_alarm_summary or UPS_Ix_power_supply_absence or UPS_Arrakis_alarm_summary or UPS_Arrakis_power_supply_absence)
    +    """
    +
    +    raise NotImplementedError
    +
    +
    +
    +

    Inherited members

    + +
    +
    +class cdaq9375Proxy +(protocol='tcp', hostname='localhost', port=7400, timeout=30000) +
    +
    +

    The cdaq9375Proxy class is used to connect to the control server and send commands to +the NI CDAQ 9375 device remotely.

    +

    Args

    +
    +
    protocol
    +
    the transport protocol [default is taken from settings file]
    +
    hostname
    +
    location of the control server (IP address) +[default is taken from settings file]
    +
    port
    +
    TCP port on which the control server is listening for commands +[default is taken from settings file]
    +
    +
    + +Expand source code + +
    class cdaq9375Proxy(Proxy, cdaq9375Interface):
    +    """The cdaq9375Proxy class is used to connect to the control server and send commands to
    +    the NI CDAQ 9375 device remotely."""
    +    def __init__(
    +        self,
    +        protocol=CTRL_SETTINGS.PROTOCOL,
    +        hostname=CTRL_SETTINGS.HOSTNAME,
    +        port=CTRL_SETTINGS.CDAQ9375.get("COMMANDING_PORT"),
    +        timeout = CTRL_SETTINGS.CDAQ9375.get("TIMEOUT") * 1000
    +    ):
    +        """
    +        Args:
    +            protocol: the transport protocol [default is taken from settings file]
    +            hostname: location of the control server (IP address)
    +                [default is taken from settings file]
    +            port: TCP port on which the control server is listening for commands
    +                [default is taken from settings file]
    +        """
    +        super().__init__(connect_address(protocol, hostname, port), timeout=timeout)
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    +
    +class cdaq9375Simulator +
    +
    +

    The cdaq9375 Simulator class.

    +
    + +Expand source code + +
    class cdaq9375Simulator(cdaq9375Interface):
    +    """
    +    The cdaq9375 Simulator class.
    +    """
    +
    +    def __init__(self):
    +        self._is_connected = True
    +        self.temp_operation = False
    +
    +    def is_connected(self):
    +        return self._is_connected
    +
    +    def is_simulator(self):
    +        return True
    +
    +    def connect(self):
    +        self._is_connected = True
    +
    +    def disconnect(self):
    +        self._is_connected = False
    +
    +    def reconnect(self):
    +        self.connect()
    +
    +    def get_id(self):
    +        pass
    +
    +    def info(self):
    +        pass
    +
    +    def get_tvac_and_ups_state(self):
    +        pass
    +
    +    def send_egse_state_to_tvac(self):
    +        pass
    +
    +    def dec2bitlist_compl(self, d:int):
    +        pass
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +def get_id(self) +
    +
    +
    +
    + +Expand source code + +
    def get_id(self):
    +    pass
    +
    +
    +
    +

    Inherited members

    + +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/api/egse/vacuum/mks/vacscan_cs.html b/docs/api/egse/ni/alarms/cdaq9375_cs.html similarity index 59% rename from docs/api/egse/vacuum/mks/vacscan_cs.html rename to docs/api/egse/ni/alarms/cdaq9375_cs.html index f1c0624..0352caf 100644 --- a/docs/api/egse/vacuum/mks/vacscan_cs.html +++ b/docs/api/egse/ni/alarms/cdaq9375_cs.html @@ -4,7 +4,7 @@ -egse.vacuum.mks.vacscan_cs API documentation +egse.ni.alarms.cdaq9375_cs API documentation @@ -19,45 +19,65 @@
    -

    Module egse.vacuum.mks.vacscan_cs

    +

    Module egse.ni.alarms.cdaq9375_cs

    Expand source code -
    import click
    +
    import argparse
    +import click
     import logging
     import sys
     
     import zmq
     
     from egse.control import ControlServer
    +from egse.control import is_control_server_active
    +
    +from egse.ni.alarms.cdaq9375 import cdaq9375Proxy
    +from egse.ni.alarms.cdaq9375_protocol import cdaq9375Protocol
     from egse.settings import Settings
    -from egse.vacuum.mks.vacscan import VacscanProtocol, VacscanProxy
    +from egse.zmq_ser import connect_address
     
    -logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL)
     
     logger = logging.getLogger(__name__)
     
    -CTRL_SETTINGS = Settings.load("MKS Vacscan Control Server")
     
    +CTRL_SETTINGS = Settings.load("NI Control Server")
    +
    +def is_cdaq9375_cs_active(timeout: float = 0.5):
    +    """Check if the CDAQ9375 Control Server is running.
    +
    +    Args:
    +        timeout (float): timeout when waiting for a reply [seconds, default=0.5]
    +    Returns:
    +        True if the control server is running and replied with the expected answer.
    +    """
    +
    +    endpoint = connect_address(
    +        CTRL_SETTINGS.PROTOCOL, CTRL_SETTINGS.HOSTNAME, CTRL_SETTINGS.CDAQ9375.get("COMMANDING_PORT")
    +    )
    +
    +    return is_control_server_active(endpoint, timeout)
     
    -class VacscanControlServer(ControlServer):
    +
    +class cdaq9375ControlServer(ControlServer):
         """
    -    VacscanControlServer - Command and monitor the Vacscan pressure sensor.
    -    This class works as a command and monitoring server to control the device remotely.
    +    cdaq9375ControlServer - Command and monitor the NI CDAQ 9375 Controllers.
    +    This class works as a command and monitoring server to control the Controller remotely.
         The sever binds to the following ZeroMQ sockets:
    -    * a REP socket that can be used as a command server. Any client can connect and
    -      send a command to the Lamp controller.
    -    * a PUB socket that serves as a monitoring server. It will send out Lamp status
    +    * a REQ-REP socket that can be used as a command server. Any client can connect and
    +      send a command to the Temp controller.
    +    * a PUB-SUP socket that serves as a monitoring server. It will send out Temp status
           information to all the connected clients every DELAY seconds.
         """
     
         def __init__(self):
             super().__init__()
     
    -        self.device_protocol = VacscanProtocol(self)
    +        self.device_protocol = cdaq9375Protocol(self)
     
             self.logger.debug(f"Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}")
     
    @@ -69,19 +89,19 @@ 

    Module egse.vacuum.mks.vacscan_cs

    return CTRL_SETTINGS.PROTOCOL def get_commanding_port(self): - return CTRL_SETTINGS.COMMANDING_PORT + return CTRL_SETTINGS.CDAQ9375.get("COMMANDING_PORT") def get_service_port(self): - return CTRL_SETTINGS.SERVICE_PORT + return CTRL_SETTINGS.CDAQ9375.get("SERVICE_PORT") def get_monitoring_port(self): - return CTRL_SETTINGS.MONITORING_PORT + return CTRL_SETTINGS.CDAQ9375.get("MONITORING_PORT") def get_storage_mnemonic(self): try: - return CTRL_SETTINGS.STORAGE_MNEMONIC + return CTRL_SETTINGS.CDAQ9375.get("STORAGE_MNEMONIC") except AttributeError: - return "IGM402" + return "DAS-CDAQ-ALARMS-EMPTY" @click.group() @@ -90,26 +110,32 @@

    Module egse.vacuum.mks.vacscan_cs

    @cli.command() -@click.option("--simulator", "--sim", is_flag=True, help="Start the Vacscan Simulator as the backend.") +@click.option("--simulator", "--sim", is_flag=True, help="Start the NI CDAQ9375 Simulator as the backend.") def start(simulator): - """Start the IGM402 Control Server.""" + """Start the NI CDAQ9375 Control Server.""" if simulator: + Settings.set_simulation_mode(True) + try: - controller = VacscanControlServer() + + controller = cdaq9375ControlServer() controller.serve() except KeyboardInterrupt: + print("Shutdown requested...exiting") except SystemExit as exit_code: + print("System Exit with code {}.".format(exit_code)) sys.exit(exit_code) except Exception: - logger.exception("Cannot start the IGM402 Control Server") + logger.exception("Cannot start the NI CDAQ9375Control Server") + # The above line does exactly the same as the traceback, but on the logger # import traceback # traceback.print_exc(file=sys.stdout) @@ -119,9 +145,9 @@

    Module egse.vacuum.mks.vacscan_cs

    @cli.command() def stop(): - """Send a 'quit_server' command to the Control Server.""" + """Send a 'quit_server' command to the NI CDAQ9375 Control Server.""" - with VacscanProxy() as proxy: + with cdaq9375Proxy() as proxy: sp = proxy.get_service_proxy() sp.quit_server() @@ -137,40 +163,75 @@

    Module egse.vacuum.mks.vacscan_cs

    +

    Functions

    +
    +
    +def is_cdaq9375_cs_active(timeout: float = 0.5) +
    +
    +

    Check if the CDAQ9375 Control Server is running.

    +

    Args

    +
    +
    timeout : float
    +
    timeout when waiting for a reply [seconds, default=0.5]
    +
    +

    Returns

    +

    True if the control server is running and replied with the expected answer.

    +
    + +Expand source code + +
    def is_cdaq9375_cs_active(timeout: float = 0.5):
    +    """Check if the CDAQ9375 Control Server is running.
    +
    +    Args:
    +        timeout (float): timeout when waiting for a reply [seconds, default=0.5]
    +    Returns:
    +        True if the control server is running and replied with the expected answer.
    +    """
    +
    +    endpoint = connect_address(
    +        CTRL_SETTINGS.PROTOCOL, CTRL_SETTINGS.HOSTNAME, CTRL_SETTINGS.CDAQ9375.get("COMMANDING_PORT")
    +    )
    +
    +    return is_control_server_active(endpoint, timeout)
    +
    +
    +

    Classes

    -
    -class VacscanControlServer +
    +class cdaq9375ControlServer
    -

    VacscanControlServer - Command and monitor the Vacscan pressure sensor. -This class works as a command and monitoring server to control the device remotely. +

    cdaq9375ControlServer - Command and monitor the NI CDAQ 9375 Controllers. +This class works as a command and monitoring server to control the Controller remotely. The sever binds to the following ZeroMQ sockets: -* a REP socket that can be used as a command server. Any client can connect and -send a command to the Lamp controller. -* a PUB socket that serves as a monitoring server. It will send out Lamp status +* a REQ-REP socket that can be used as a command server. Any client can connect and +send a command to the Temp controller. +* a PUB-SUP socket that serves as a monitoring server. It will send out Temp status information to all the connected clients every DELAY seconds.

    Expand source code -
    class VacscanControlServer(ControlServer):
    +
    class cdaq9375ControlServer(ControlServer):
         """
    -    VacscanControlServer - Command and monitor the Vacscan pressure sensor.
    -    This class works as a command and monitoring server to control the device remotely.
    +    cdaq9375ControlServer - Command and monitor the NI CDAQ 9375 Controllers.
    +    This class works as a command and monitoring server to control the Controller remotely.
         The sever binds to the following ZeroMQ sockets:
    -    * a REP socket that can be used as a command server. Any client can connect and
    -      send a command to the Lamp controller.
    -    * a PUB socket that serves as a monitoring server. It will send out Lamp status
    +    * a REQ-REP socket that can be used as a command server. Any client can connect and
    +      send a command to the Temp controller.
    +    * a PUB-SUP socket that serves as a monitoring server. It will send out Temp status
           information to all the connected clients every DELAY seconds.
         """
     
         def __init__(self):
             super().__init__()
     
    -        self.device_protocol = VacscanProtocol(self)
    +        self.device_protocol = cdaq9375Protocol(self)
     
             self.logger.debug(f"Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}")
     
    @@ -182,19 +243,19 @@ 

    Classes

    return CTRL_SETTINGS.PROTOCOL def get_commanding_port(self): - return CTRL_SETTINGS.COMMANDING_PORT + return CTRL_SETTINGS.CDAQ9375.get("COMMANDING_PORT") def get_service_port(self): - return CTRL_SETTINGS.SERVICE_PORT + return CTRL_SETTINGS.CDAQ9375.get("SERVICE_PORT") def get_monitoring_port(self): - return CTRL_SETTINGS.MONITORING_PORT + return CTRL_SETTINGS.CDAQ9375.get("MONITORING_PORT") def get_storage_mnemonic(self): try: - return CTRL_SETTINGS.STORAGE_MNEMONIC + return CTRL_SETTINGS.CDAQ9375.get("STORAGE_MNEMONIC") except AttributeError: - return "IGM402"
    + return "DAS-CDAQ-ALARMS-EMPTY"

    Ancestors

      @@ -202,7 +263,7 @@

      Ancestors

    Methods

    -
    +
    def get_commanding_port(self)
    @@ -212,10 +273,10 @@

    Methods

    Expand source code
    def get_commanding_port(self):
    -    return CTRL_SETTINGS.COMMANDING_PORT
    + return CTRL_SETTINGS.CDAQ9375.get("COMMANDING_PORT")
    -
    +
    def get_communication_protocol(self)
    @@ -228,7 +289,7 @@

    Methods

    return CTRL_SETTINGS.PROTOCOL
    -
    +
    def get_monitoring_port(self)
    @@ -238,10 +299,10 @@

    Methods

    Expand source code
    def get_monitoring_port(self):
    -    return CTRL_SETTINGS.MONITORING_PORT
    + return CTRL_SETTINGS.CDAQ9375.get("MONITORING_PORT")
    -
    +
    def get_service_port(self)
    @@ -251,10 +312,10 @@

    Methods

    Expand source code
    def get_service_port(self):
    -    return CTRL_SETTINGS.SERVICE_PORT
    + return CTRL_SETTINGS.CDAQ9375.get("SERVICE_PORT")
    -
    +
    def get_storage_mnemonic(self)
    @@ -265,9 +326,9 @@

    Methods

    def get_storage_mnemonic(self):
         try:
    -        return CTRL_SETTINGS.STORAGE_MNEMONIC
    +        return CTRL_SETTINGS.CDAQ9375.get("STORAGE_MNEMONIC")
         except AttributeError:
    -        return "IGM402"
    + return "DAS-CDAQ-ALARMS-EMPTY"
    @@ -275,10 +336,15 @@

    Inherited members

    • ControlServer:
    • @@ -295,19 +361,24 @@

      Index

      • Super-module

        +
      • +
      • Functions

        +
      • Classes

        diff --git a/docs/api/egse/ni/alarms/cdaq9375_devif.html b/docs/api/egse/ni/alarms/cdaq9375_devif.html new file mode 100644 index 0000000..88e301d --- /dev/null +++ b/docs/api/egse/ni/alarms/cdaq9375_devif.html @@ -0,0 +1,602 @@ + + + + + + +egse.ni.alarms.cdaq9375_devif API documentation + + + + + + + + + + + +
        +
        +
        +

        Module egse.ni.alarms.cdaq9375_devif

        +
        +
        +

        The device interface for the NICDAQ 9375 Controller which is used at IAS for alarms system.

        +
        + +Expand source code + +
        """
        +The device interface for the NICDAQ 9375 Controller which is used at IAS for alarms system.
        +
        +"""
        +
        +import logging
        +import time
        +import socket
        +import struct
        +from typing import List
        +
        +from egse.settings import Settings
        +from egse.exceptions import DeviceNotFoundError
        +from egse.device import DeviceConnectionError, DeviceError
        +from egse.device import DeviceConnectionInterface, DeviceTransport, DeviceTimeoutError
        +from egse.command import ClientServerCommand
        +
        +logger = logging.getLogger(__name__)
        +
        +ctrl_settings = Settings.load("NI Controller")
        +
        +DEVICE_NAME = "CDAQ9375"
        +
        +READ_TIMEOUT = 1
        +
        +
        +class cdaq9375Error(Exception):
        +    """Base exception for all ptc10 errors."""
        +    pass
        +
        +
        +class cdaq9375Command(ClientServerCommand):
        +    def get_cmd_string(self, *args, **kwargs) -> str:
        +        out = super().get_cmd_string(*args, **kwargs)
        +        return out + "\n"
        +
        +
        +class cdaq9375SocketInterface(DeviceConnectionInterface, DeviceTransport):
        +    """Defines the low-level interface to the NI CDAQ9375 Controller.
        +    Connects to the Labview interface via TCP/IP socket"""
        +
        +    def __init__(self, hostname=None, port=None):
        +        self.hostname = ctrl_settings.HOSTNAME if hostname is None else hostname
        +        self.port = ctrl_settings.CDAQ9375_PORT if port is None else port
        +        self.sock = None
        +
        +        self.is_connection_open = False
        +
        +    def connect(self, hostname: str):
        +        # Sanity checks
        +        if self.is_connection_open:
        +            logger.warning(f"{DEVICE_NAME}: trying to connect to an already connected socket.")
        +            return
        +        if self.hostname in (None, ""):
        +            raise ValueError(f"{DEVICE_NAME}: hostname is not initialized.")
        +
        +        if self.port in (None, 0):
        +            raise ValueError(f"{DEVICE_NAME}: port number is not initialized.")
        +
        +        try:
        +            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        +            _id = self.get_id()
        +            logger.warning(f"Connected to: {_id}")
        +
        +        except socket.error as e_socket:
        +            raise DeviceConnectionError(DEVICE_NAME, "Failed to create socket.") from e_socket
        +            # Asking for instrument description
        +
        +        # We set a timeout of 3 sec before connecting and reset to None
        +        # (=blocking) after the connect. The reason for this is because when no
        +        # device is available, e.g during testing, the timeout will take about
        +        # two minutes which is way too long. It needs to be evaluated if this
        +        # approach is acceptable and not causing problems during production.
        +
        +        try:
        +            logger.debug(f'Connecting a socket to host "{self.hostname}" using port {self.port}')
        +            self.sock.settimeout(3)
        +            self.sock.connect((self.hostname, self.port))
        +            self.sock.settimeout(None)
        +        except ConnectionRefusedError as exc:
        +            raise DeviceConnectionError(
        +                DEVICE_NAME, f"Connection refused to {self.hostname}:{self.port}."
        +            ) from exc
        +        except TimeoutError as exc:
        +            raise DeviceTimeoutError(
        +                DEVICE_NAME, f"Connection to {self.hostname}:{self.port} timed out."
        +            ) from exc
        +        except socket.gaierror as exc:
        +            raise DeviceConnectionError(
        +                DEVICE_NAME, f"socket address info error for {self.hostname}"
        +            ) from exc
        +        except socket.herror as exc:
        +            raise DeviceConnectionError(
        +                DEVICE_NAME, f"socket host address error for {self.hostname}"
        +            ) from exc
        +        except socket.timeout as exc:
        +            raise DeviceTimeoutError(
        +                DEVICE_NAME, f"socket timeout error for {self.hostname}:{self.port}"
        +            ) from exc
        +        except OSError as exc:
        +            raise DeviceConnectionError(DEVICE_NAME, f"OSError caught ({exc}).") from exc
        +
        +        self.is_connection_open = True
        +
        +        if not self.is_connected():
        +            raise DeviceConnectionError(
        +                DEVICE_NAME, "Device is not connected, check logging messages for the cause."
        +            )
        +
        +    def disconnect(self):
        +        """
        +        Disconnects from the device
        +        Raises:
        +            DeviceConnectionError when the connection has not been closed correctly
        +
        +        """
        +        try:
        +            if self.is_connection_open:
        +                logger.debug(f"Disconnecting from {self.hostname}")
        +                self.sock.close()
        +                self.is_connection_open = False
        +        except Exception as e_exc:
        +            raise DeviceConnectionError(
        +                DEVICE_NAME, f"Could not close socket to {self.hostname}") from e_exc
        +
        +    def is_connected(self):
        +        """Return True if the device is connected."""
        +        if not self.is_connection_open:
        +            return False
        +        return True
        +
        +    def get_response(self, cmd_string):
        +        pass
        +
        +    def get_id(self):
        +        return DEVICE_NAME
        +
        +    def write(self, command: str):
        +        try:
        +            #logger.info(f"{command=}")
        +            self.sock.sendall(command.encode())
        +
        +        except socket.timeout as e_timeout:
        +            raise DeviceTimeoutError(DEVICE_NAME, "Socket timeout error") from e_timeout
        +        except socket.error as e_socket:
        +            # Interpret any socket-related error as a connection error
        +            raise DeviceConnectionError(DEVICE_NAME, "Socket communication error.") from e_socket
        +        except AttributeError:
        +            if not self.is_connection_open:
        +                msg = "The CDAQ9375 is not connected, use the connect() method."
        +                raise DeviceConnectionError(DEVICE_NAME, msg)
        +            raise
        +
        +    def read(self) -> List:
        +        # Set a timeout of READ_TIMEOUT to the socket.recv
        +        saved_timeout = self.sock.gettimeout()
        +        self.sock.settimeout(READ_TIMEOUT)
        +        try:
        +            # Extracts the msg size from 4 bytes sent by Labview - mind the encoding. The number
        +            # reprensets the number of bytes of data
        +            size = struct.unpack('i', self.sock.recv(4))[0]
        +            data = self.sock.recv(size)
        +        except socket.timeout as e_timeout:
        +            logger.warning(f"Socket timeout error from {e_timeout}")
        +            return b"\r\n"
        +        except TimeoutError as exc:
        +            logger.warning(f"Socket timeout error: {exc}")
        +            return b"\r\n"
        +        finally:
        +            self.sock.settimeout(saved_timeout)
        +
        +        #logger.info(f"Total number of bytes received from the cdaq 9375 is {size}")
        +
        +        data = data.decode("ascii")
        +
        +        return data
        +
        +    def trans(self, cmd: str) -> List:
        +        self.write(cmd)
        +        time.sleep(25)
        +        response = self.read()
        +        return response
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +

        Classes

        +
        +
        +class cdaq9375Command +(name, cmd, response=None, wait=None, check=None, description=None, device_method=None) +
        +
        +

        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.

        +
        + +Expand source code + +
        class cdaq9375Command(ClientServerCommand):
        +    def get_cmd_string(self, *args, **kwargs) -> str:
        +        out = super().get_cmd_string(*args, **kwargs)
        +        return out + "\n"
        +
        +

        Ancestors

        + +

        Methods

        +
        +
        +def get_cmd_string(self, *args, **kwargs) ‑> str +
        +
        +
        +
        + +Expand source code + +
        def get_cmd_string(self, *args, **kwargs) -> str:
        +    out = super().get_cmd_string(*args, **kwargs)
        +    return out + "\n"
        +
        +
        +
        +

        Inherited members

        + +
        +
        +class cdaq9375Error +(*args, **kwargs) +
        +
        +

        Base exception for all ptc10 errors.

        +
        + +Expand source code + +
        class cdaq9375Error(Exception):
        +    """Base exception for all ptc10 errors."""
        +    pass
        +
        +

        Ancestors

        +
          +
        • builtins.Exception
        • +
        • builtins.BaseException
        • +
        +
        +
        +class cdaq9375SocketInterface +(hostname=None, port=None) +
        +
        +

        Defines the low-level interface to the NI CDAQ9375 Controller. +Connects to the Labview interface via TCP/IP socket

        +
        + +Expand source code + +
        class cdaq9375SocketInterface(DeviceConnectionInterface, DeviceTransport):
        +    """Defines the low-level interface to the NI CDAQ9375 Controller.
        +    Connects to the Labview interface via TCP/IP socket"""
        +
        +    def __init__(self, hostname=None, port=None):
        +        self.hostname = ctrl_settings.HOSTNAME if hostname is None else hostname
        +        self.port = ctrl_settings.CDAQ9375_PORT if port is None else port
        +        self.sock = None
        +
        +        self.is_connection_open = False
        +
        +    def connect(self, hostname: str):
        +        # Sanity checks
        +        if self.is_connection_open:
        +            logger.warning(f"{DEVICE_NAME}: trying to connect to an already connected socket.")
        +            return
        +        if self.hostname in (None, ""):
        +            raise ValueError(f"{DEVICE_NAME}: hostname is not initialized.")
        +
        +        if self.port in (None, 0):
        +            raise ValueError(f"{DEVICE_NAME}: port number is not initialized.")
        +
        +        try:
        +            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        +            _id = self.get_id()
        +            logger.warning(f"Connected to: {_id}")
        +
        +        except socket.error as e_socket:
        +            raise DeviceConnectionError(DEVICE_NAME, "Failed to create socket.") from e_socket
        +            # Asking for instrument description
        +
        +        # We set a timeout of 3 sec before connecting and reset to None
        +        # (=blocking) after the connect. The reason for this is because when no
        +        # device is available, e.g during testing, the timeout will take about
        +        # two minutes which is way too long. It needs to be evaluated if this
        +        # approach is acceptable and not causing problems during production.
        +
        +        try:
        +            logger.debug(f'Connecting a socket to host "{self.hostname}" using port {self.port}')
        +            self.sock.settimeout(3)
        +            self.sock.connect((self.hostname, self.port))
        +            self.sock.settimeout(None)
        +        except ConnectionRefusedError as exc:
        +            raise DeviceConnectionError(
        +                DEVICE_NAME, f"Connection refused to {self.hostname}:{self.port}."
        +            ) from exc
        +        except TimeoutError as exc:
        +            raise DeviceTimeoutError(
        +                DEVICE_NAME, f"Connection to {self.hostname}:{self.port} timed out."
        +            ) from exc
        +        except socket.gaierror as exc:
        +            raise DeviceConnectionError(
        +                DEVICE_NAME, f"socket address info error for {self.hostname}"
        +            ) from exc
        +        except socket.herror as exc:
        +            raise DeviceConnectionError(
        +                DEVICE_NAME, f"socket host address error for {self.hostname}"
        +            ) from exc
        +        except socket.timeout as exc:
        +            raise DeviceTimeoutError(
        +                DEVICE_NAME, f"socket timeout error for {self.hostname}:{self.port}"
        +            ) from exc
        +        except OSError as exc:
        +            raise DeviceConnectionError(DEVICE_NAME, f"OSError caught ({exc}).") from exc
        +
        +        self.is_connection_open = True
        +
        +        if not self.is_connected():
        +            raise DeviceConnectionError(
        +                DEVICE_NAME, "Device is not connected, check logging messages for the cause."
        +            )
        +
        +    def disconnect(self):
        +        """
        +        Disconnects from the device
        +        Raises:
        +            DeviceConnectionError when the connection has not been closed correctly
        +
        +        """
        +        try:
        +            if self.is_connection_open:
        +                logger.debug(f"Disconnecting from {self.hostname}")
        +                self.sock.close()
        +                self.is_connection_open = False
        +        except Exception as e_exc:
        +            raise DeviceConnectionError(
        +                DEVICE_NAME, f"Could not close socket to {self.hostname}") from e_exc
        +
        +    def is_connected(self):
        +        """Return True if the device is connected."""
        +        if not self.is_connection_open:
        +            return False
        +        return True
        +
        +    def get_response(self, cmd_string):
        +        pass
        +
        +    def get_id(self):
        +        return DEVICE_NAME
        +
        +    def write(self, command: str):
        +        try:
        +            #logger.info(f"{command=}")
        +            self.sock.sendall(command.encode())
        +
        +        except socket.timeout as e_timeout:
        +            raise DeviceTimeoutError(DEVICE_NAME, "Socket timeout error") from e_timeout
        +        except socket.error as e_socket:
        +            # Interpret any socket-related error as a connection error
        +            raise DeviceConnectionError(DEVICE_NAME, "Socket communication error.") from e_socket
        +        except AttributeError:
        +            if not self.is_connection_open:
        +                msg = "The CDAQ9375 is not connected, use the connect() method."
        +                raise DeviceConnectionError(DEVICE_NAME, msg)
        +            raise
        +
        +    def read(self) -> List:
        +        # Set a timeout of READ_TIMEOUT to the socket.recv
        +        saved_timeout = self.sock.gettimeout()
        +        self.sock.settimeout(READ_TIMEOUT)
        +        try:
        +            # Extracts the msg size from 4 bytes sent by Labview - mind the encoding. The number
        +            # reprensets the number of bytes of data
        +            size = struct.unpack('i', self.sock.recv(4))[0]
        +            data = self.sock.recv(size)
        +        except socket.timeout as e_timeout:
        +            logger.warning(f"Socket timeout error from {e_timeout}")
        +            return b"\r\n"
        +        except TimeoutError as exc:
        +            logger.warning(f"Socket timeout error: {exc}")
        +            return b"\r\n"
        +        finally:
        +            self.sock.settimeout(saved_timeout)
        +
        +        #logger.info(f"Total number of bytes received from the cdaq 9375 is {size}")
        +
        +        data = data.decode("ascii")
        +
        +        return data
        +
        +    def trans(self, cmd: str) -> List:
        +        self.write(cmd)
        +        time.sleep(25)
        +        response = self.read()
        +        return response
        +
        +

        Ancestors

        + +

        Methods

        +
        +
        +def disconnect(self) +
        +
        +

        Disconnects from the device

        +

        Raises

        +

        DeviceConnectionError when the connection has not been closed correctly

        +
        + +Expand source code + +
        def disconnect(self):
        +    """
        +    Disconnects from the device
        +    Raises:
        +        DeviceConnectionError when the connection has not been closed correctly
        +
        +    """
        +    try:
        +        if self.is_connection_open:
        +            logger.debug(f"Disconnecting from {self.hostname}")
        +            self.sock.close()
        +            self.is_connection_open = False
        +    except Exception as e_exc:
        +        raise DeviceConnectionError(
        +            DEVICE_NAME, f"Could not close socket to {self.hostname}") from e_exc
        +
        +
        +
        +def get_id(self) +
        +
        +
        +
        + +Expand source code + +
        def get_id(self):
        +    return DEVICE_NAME
        +
        +
        +
        +def get_response(self, cmd_string) +
        +
        +
        +
        + +Expand source code + +
        def get_response(self, cmd_string):
        +    pass
        +
        +
        +
        +def is_connected(self) +
        +
        +

        Return True if the device is connected.

        +
        + +Expand source code + +
        def is_connected(self):
        +    """Return True if the device is connected."""
        +    if not self.is_connection_open:
        +        return False
        +    return True
        +
        +
        +
        +

        Inherited members

        + +
        +
        +
        +
        + +
        + + + \ No newline at end of file diff --git a/docs/api/egse/tempcontrol/lakeshore/lsci336_protocol.html b/docs/api/egse/ni/alarms/cdaq9375_protocol.html similarity index 84% rename from docs/api/egse/tempcontrol/lakeshore/lsci336_protocol.html rename to docs/api/egse/ni/alarms/cdaq9375_protocol.html index 09822b2..45b614e 100644 --- a/docs/api/egse/tempcontrol/lakeshore/lsci336_protocol.html +++ b/docs/api/egse/ni/alarms/cdaq9375_protocol.html @@ -4,7 +4,7 @@ -egse.tempcontrol.lakeshore.lsci336_protocol API documentation +egse.ni.alarms.cdaq9375_protocol API documentation @@ -19,39 +19,47 @@
        -

        Module egse.tempcontrol.lakeshore.lsci336_protocol

        +

        Module egse.ni.alarms.cdaq9375_protocol

        Expand source code -
        from egse.control import ControlServer
        +
        import logging
        +import datetime
        +
        +
        +from egse.control import ControlServer
        +from egse.ni.alarms.cdaq9375 import cdaq9375Controller, cdaq9375Simulator, cdaq9375Interface
        +from egse.ni.alarms.cdaq9375_devif import cdaq9375Command
        +
         from egse.protocol import CommandProtocol
        +from egse.device import DeviceConnectionState
         from egse.settings import Settings
         from egse.system import format_datetime
        -from egse.tempcontrol.lakeshore.lsci336 import LakeShore336Controller
        -from egse.tempcontrol.lakeshore.lsci336 import LakeShore336Interface
        -from egse.tempcontrol.lakeshore.lsci336 import LakeShore336Simulator
        -from egse.tempcontrol.lakeshore.lsci336_devif import LakeShore336Command
         from egse.zmq_ser import bind_address
         
        -COMMAND_SETTINGS = Settings.load(filename="lsci336.yaml")
        +COMMAND_SETTINGS = Settings.load(filename="cdaq9375.yaml")
        +logger = logging.getLogger(__name__)
        +
         
         
        -class LakeShore336Protocol(CommandProtocol):
        +class cdaq9375Protocol(CommandProtocol):
             def __init__(self, control_server: ControlServer):
                 super().__init__()
                 self.control_server = control_server
         
                 if Settings.simulation_mode():
        -            self.lakeshore = LakeShore336Simulator()
        +            self.cdaq = cdaq9375Simulator()
                 else:
        -            self.lakeshore = LakeShore336Controller()
        +            self.cdaq = cdaq9375Controller()
         
        -        self.load_commands(COMMAND_SETTINGS.Commands, LakeShore336Command, LakeShore336Interface)
        +        self.load_commands(
        +            COMMAND_SETTINGS.Commands, cdaq9375Command, cdaq9375Interface
        +        )
         
        -        self.build_device_method_lookup_table(self.lakeshore)
        +        self.build_device_method_lookup_table(self.cdaq)
         
             def get_bind_address(self):
                 return bind_address(
        @@ -63,10 +71,9 @@ 

        Module egse.tempcontrol.lakeshore.lsci336_protocol

        + + result = dict() + return result
        @@ -78,8 +85,8 @@

        Module egse.tempcontrol.lakeshore.lsci336_protocol

        Classes

        -
        -class LakeShore336Protocol +
        +class cdaq9375Protocol (control_server: ControlServer)
        @@ -95,19 +102,21 @@

        Classes

        Expand source code -
        class LakeShore336Protocol(CommandProtocol):
        +
        class cdaq9375Protocol(CommandProtocol):
             def __init__(self, control_server: ControlServer):
                 super().__init__()
                 self.control_server = control_server
         
                 if Settings.simulation_mode():
        -            self.lakeshore = LakeShore336Simulator()
        +            self.cdaq = cdaq9375Simulator()
                 else:
        -            self.lakeshore = LakeShore336Controller()
        +            self.cdaq = cdaq9375Controller()
         
        -        self.load_commands(COMMAND_SETTINGS.Commands, LakeShore336Command, LakeShore336Interface)
        +        self.load_commands(
        +            COMMAND_SETTINGS.Commands, cdaq9375Command, cdaq9375Interface
        +        )
         
        -        self.build_device_method_lookup_table(self.lakeshore)
        +        self.build_device_method_lookup_table(self.cdaq)
         
             def get_bind_address(self):
                 return bind_address(
        @@ -119,10 +128,9 @@ 

        Classes

        return super().get_status() def get_housekeeping(self) -> dict: - return { - "timestamp": format_datetime(), - "Temp A": self.lakeshore.get_temperature("A"), - }
        + + result = dict() + return result

        Ancestors

          @@ -162,13 +170,13 @@

          Index

          • Super-module

          • Classes

          • diff --git a/docs/api/egse/ni/alarms/index.html b/docs/api/egse/ni/alarms/index.html new file mode 100644 index 0000000..84f619e --- /dev/null +++ b/docs/api/egse/ni/alarms/index.html @@ -0,0 +1,129 @@ + + + + + + +egse.ni.alarms API documentation + + + + + + + + + + + +
            +
            +
            +

            Module egse.ni.alarms

            +
            +
            +

            Device control for the NI cdaq9375 that will be used at IAS for alarm manager.

            +

            This package contains the classes and modules to work with the Labview code running on Windows Keynes dealing with +CDAQ9375 .

            +

            The main entry point for the user of this package is through the cdaq9375Proxy class:

            +
            >>> from egse.ni.alarms.cdaq9375 import cdaq9375Proxy
            +
            +

            This class will connect to the control server of the CDAQ 9375 Controller and provides all +commands to +get alarms from the 2 EGSE UPS and from TVAC and send EGSE alarms to TVAC. The control server is a small +server application +that is started as shown below. The control server directly connects to the cdaq9375 readout Controller +through an Ethernet +interface. When you have no hardware available, the control server can be started in simulator +mode by appending the +--sim string to the command below.

            +
            $ python -m egse.ni.alarms.cdaq9375_cs
            +
            +
            + +Expand source code + +
            """
            +Device control for the NI cdaq9375 that will be used at IAS for alarm manager.
            +
            +This package contains the classes and modules to work with the Labview code running on Windows Keynes dealing with
            +CDAQ9375 .
            +
            +The main entry point for the user of this package is through the `cdaq9375Proxy` class:
            +
            +```python
            +>>> from egse.ni.alarms.cdaq9375 import cdaq9375Proxy
            +```
            +
            +This class will connect to the control server of the CDAQ 9375 Controller and provides all
            +commands to
            +get alarms from the 2 EGSE UPS and from TVAC and send EGSE alarms to TVAC. The control server is a small
            +server application
            +that is started as shown below. The control server directly connects to the cdaq9375 readout Controller
            +through an Ethernet
            +interface. When you have no hardware available, the control server can be started in simulator
            +mode by appending the
            +`--sim` string to the command below.
            +
            +```bash
            +$ python -m egse.ni.alarms.cdaq9375_cs
            +```
            +"""
            +
            +
            +
            +

            Sub-modules

            +
            +
            egse.ni.alarms.cdaq9375
            +
            +

            This module defines the basic classes to access the UPS and TVAC info and send alarms to TVAC readout ensured by +NI CDAQ9375 controller that is used.

            +
            +
            egse.ni.alarms.cdaq9375_cs
            +
            +
            +
            +
            egse.ni.alarms.cdaq9375_devif
            +
            +

            The device interface for the NICDAQ 9375 Controller which is used at IAS for alarms system.

            +
            +
            egse.ni.alarms.cdaq9375_protocol
            +
            +
            +
            +
            +
            +
            +
            +
            +
            +
            +
            +
            + +
            + + + \ No newline at end of file diff --git a/docs/api/egse/tempcontrol/beaglebone/index.html b/docs/api/egse/ni/index.html similarity index 75% rename from docs/api/egse/tempcontrol/beaglebone/index.html rename to docs/api/egse/ni/index.html index cc6dceb..77c9e95 100644 --- a/docs/api/egse/tempcontrol/beaglebone/index.html +++ b/docs/api/egse/ni/index.html @@ -4,7 +4,7 @@ -egse.tempcontrol.beaglebone API documentation +egse.ni API documentation @@ -19,32 +19,16 @@
            -

            Namespace egse.tempcontrol.beaglebone

            +

            Namespace egse.ni

            Sub-modules

            -
            egse.tempcontrol.beaglebone.beaglebone
            +
            egse.ni.alarms
            -
            -
            -
            egse.tempcontrol.beaglebone.beaglebone_cs
            -
            -
            -
            -
            egse.tempcontrol.beaglebone.beaglebone_devif
            -
            -
            -
            -
            egse.tempcontrol.beaglebone.beaglebone_protocol
            -
            -
            -
            -
            egse.tempcontrol.beaglebone.beaglebone_ui
            -
            -
            +

            Device control for the NI cdaq9375 that will be used at IAS for alarm manager …

            @@ -63,16 +47,12 @@

            Index

            diff --git a/docs/api/egse/obs_inspection.html b/docs/api/egse/obs_inspection.html index 90c4d92..879e05d 100644 --- a/docs/api/egse/obs_inspection.html +++ b/docs/api/egse/obs_inspection.html @@ -35,8 +35,8 @@

            Module egse.obs_inspection

            from egse import h5 from egse.dpu.fitsgen import get_hdf5_filenames_for_obsid from egse.fee.nfee import HousekeepingData +from egse.setup import load_setup from egse.spw import DataPacket, HousekeepingPacket -from egse.state import GlobalState def parse_arguments(): @@ -63,8 +63,10 @@

            Module egse.obs_inspection

            num_hdf5_files = len(obs_hdf5_filenames) print(f"Found {num_hdf5_files} HDF5 files for obsid {obsid}") - ccd_bin_to_id = GlobalState.setup.camera.fee.ccd_numbering.CCD_BIN_TO_ID - n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum + setup = load_setup() + + ccd_bin_to_id = setup.camera.fee.ccd_numbering.CCD_BIN_TO_ID + n_fee_side = setup.camera.fee.ccd_sides.enum table = Table(title=f"Report for obsid {obsid}", expand=False) table.add_column("HDF5", justify="right") @@ -239,8 +241,10 @@

            Functions

            num_hdf5_files = len(obs_hdf5_filenames) print(f"Found {num_hdf5_files} HDF5 files for obsid {obsid}") - ccd_bin_to_id = GlobalState.setup.camera.fee.ccd_numbering.CCD_BIN_TO_ID - n_fee_side = GlobalState.setup.camera.fee.ccd_sides.enum + setup = load_setup() + + ccd_bin_to_id = setup.camera.fee.ccd_numbering.CCD_BIN_TO_ID + n_fee_side = setup.camera.fee.ccd_sides.enum table = Table(title=f"Report for obsid {obsid}", expand=False) table.add_column("HDF5", justify="right") diff --git a/docs/api/egse/observer.html b/docs/api/egse/observer.html index 475bae8..641150e 100644 --- a/docs/api/egse/observer.html +++ b/docs/api/egse/observer.html @@ -120,7 +120,7 @@

            Subclasses

            Methods

            @@ -257,7 +256,7 @@

            Subclasses

            Methods

            diff --git a/docs/api/egse/powermeter/ni/cdaq9184.html b/docs/api/egse/powermeter/ni/cdaq9184.html index 6600236..76168cd 100644 --- a/docs/api/egse/powermeter/ni/cdaq9184.html +++ b/docs/api/egse/powermeter/ni/cdaq9184.html @@ -144,6 +144,12 @@

            Module egse.powermeter.ni.cdaq9184

            def read_values(self): pass + def read_photodiodes(self): + pass + + def read_temperatures(self): + pass + class cdaq9184Controller(cdaq9184Interface): """The cdaq9184Controller allows controlling a NI CDAQ 9184 photodiode measurement device.""" @@ -209,25 +215,23 @@

            Module egse.powermeter.ni.cdaq9184

            def read_values(self) -> list: data = self.cdaq.trans("get_value") + return data - # The 3 last elements of data are expected not to be a number but the position of the filter wheel - # (wheel 1 and 2) and the position of the shutter. - # As we don't need these info, I delete them. - # But to avoid to delete a relevant value I check if the element is a number or not before deleting. - b = True - while b: - b = False - try: - float(data[-1]) - except ValueError: - del data[-1] - b = True - - # The 2 first elements of data are date as str type. I leave them as str type. I convert the other elements of - # data (which are relevant values) from str to float. - data = data[:2] + [float(value) for value in data[2:]] + def read_temperatures(self) -> dict: + values = self.read_values() + all_temp = values[14:26] - return data + temp_mean = [all_temp[i] for i in range(0, len(all_temp), 2)] + temp_mean = values[2:] + temp_mean # c'est pas plutôt values[:2] ? + temp_std = [all_temp[i] for i in range(1, len(all_temp), 2)] + temp_std = values[2:] + temp_std # c'est pas plutôt values[:2] ? + + temperatures = {"temp_mean": temp_mean, "temp_std": temp_std} + + return temperatures + + def read_photodiodes(self): + pass class cdaq9184Proxy(Proxy, cdaq9184Interface): @@ -333,25 +337,23 @@

            Classes

            def read_values(self) -> list: data = self.cdaq.trans("get_value") + return data + + def read_temperatures(self) -> dict: + values = self.read_values() + all_temp = values[14:26] + + temp_mean = [all_temp[i] for i in range(0, len(all_temp), 2)] + temp_mean = values[2:] + temp_mean # c'est pas plutôt values[:2] ? + temp_std = [all_temp[i] for i in range(1, len(all_temp), 2)] + temp_std = values[2:] + temp_std # c'est pas plutôt values[:2] ? + + temperatures = {"temp_mean": temp_mean, "temp_std": temp_std} - # The 3 last elements of data are expected not to be a number but the position of the filter wheel - # (wheel 1 and 2) and the position of the shutter. - # As we don't need these info, I delete them. - # But to avoid to delete a relevant value I check if the element is a number or not before deleting. - b = True - while b: - b = False - try: - float(data[-1]) - except ValueError: - del data[-1] - b = True - - # The 2 first elements of data are date as str type. I leave them as str type. I convert the other elements of - # data (which are relevant values) from str to float. - data = data[:2] + [float(value) for value in data[2:]] - - return data
            + return temperatures + + def read_photodiodes(self): + pass

            Ancestors

              @@ -762,6 +764,12 @@

              Inherited members

              pass def read_values(self): + pass + + def read_photodiodes(self): + pass + + def read_temperatures(self): pass

              Ancestors

              diff --git a/docs/api/egse/powermeter/ni/cdaq9184_cs.html b/docs/api/egse/powermeter/ni/cdaq9184_cs.html index 6173d9e..124112a 100644 --- a/docs/api/egse/powermeter/ni/cdaq9184_cs.html +++ b/docs/api/egse/powermeter/ni/cdaq9184_cs.html @@ -101,7 +101,7 @@

              Module egse.powermeter.ni.cdaq9184_cs

              try: return CTRL_SETTINGS.CDAQ9184.get("STORAGE_MNEMONIC") except AttributeError: - return "CDAQ9184" + return "DAS-CDAQ-PHOTODIODES-EMPTY" @click.group() @@ -255,7 +255,7 @@

              Classes

              try: return CTRL_SETTINGS.CDAQ9184.get("STORAGE_MNEMONIC") except AttributeError: - return "CDAQ9184" + return "DAS-CDAQ-PHOTODIODES-EMPTY"

              Ancestors

                @@ -328,7 +328,7 @@

                Methods

                try: return CTRL_SETTINGS.CDAQ9184.get("STORAGE_MNEMONIC") except AttributeError: - return "CDAQ9184" + return "DAS-CDAQ-PHOTODIODES-EMPTY"
        @@ -336,10 +336,15 @@

        Inherited members

        • ControlServer:
        • diff --git a/docs/api/egse/powermeter/ni/cdaq9184_devif.html b/docs/api/egse/powermeter/ni/cdaq9184_devif.html index 24c4be3..1df3fe5 100644 --- a/docs/api/egse/powermeter/ni/cdaq9184_devif.html +++ b/docs/api/egse/powermeter/ni/cdaq9184_devif.html @@ -70,7 +70,7 @@

          Module egse.powermeter.ni.cdaq9184_devif

          def __init__(self, hostname=None, port=None): self.hostname = ctrl_settings.HOSTNAME if hostname is None else hostname - self.port = ctrl_settings.CDAQ9184_PORT if port is None else port + self.port = ctrl_settings.CDAQ9174_PORT if port is None else port self.sock = None self.is_connection_open = False @@ -209,18 +209,17 @@

          Module egse.powermeter.ni.cdaq9184_devif

          data = data.replace("\r", ",") data = data.replace("\x04", ",") data = data.replace("\x12", ",") + data = data.replace("\x0e", ",") data = data.replace(" ", ",") data = data.replace("0^", "E") data = data.split(",") data = [element.strip() for element in data] - # The first element of data is "#" and the last is an empty string so I delete them. - # But to avoid to delete a relevant value I check if they're well in data before deleting. + # The first element of data is "#" so I delete it. + # But to avoid to delete a relevant value I check if this unwanted element is well in data before deleting. if data[0] == "#": del data[0] - if data[-1] == "": - del data[-1] return data @@ -328,7 +327,7 @@

          Ancestors

          def __init__(self, hostname=None, port=None): self.hostname = ctrl_settings.HOSTNAME if hostname is None else hostname - self.port = ctrl_settings.CDAQ9184_PORT if port is None else port + self.port = ctrl_settings.CDAQ9174_PORT if port is None else port self.sock = None self.is_connection_open = False @@ -467,18 +466,17 @@

          Ancestors

          data = data.replace("\r", ",") data = data.replace("\x04", ",") data = data.replace("\x12", ",") + data = data.replace("\x0e", ",") data = data.replace(" ", ",") data = data.replace("0^", "E") data = data.split(",") data = [element.strip() for element in data] - # The first element of data is "#" and the last is an empty string so I delete them. - # But to avoid to delete a relevant value I check if they're well in data before deleting. + # The first element of data is "#" so I delete it. + # But to avoid to delete a relevant value I check if this unwanted element is well in data before deleting. if data[0] == "#": del data[0] - if data[-1] == "": - del data[-1] return data diff --git a/docs/api/egse/powermeter/ni/cdaq9184_protocol.html b/docs/api/egse/powermeter/ni/cdaq9184_protocol.html index 7355d7c..d75b180 100644 --- a/docs/api/egse/powermeter/ni/cdaq9184_protocol.html +++ b/docs/api/egse/powermeter/ni/cdaq9184_protocol.html @@ -73,34 +73,6 @@

          Module egse.powermeter.ni.cdaq9184_protocol

          def get_housekeeping(self) -> dict: result = dict() - result["timestamp"] = format_datetime() - - # FIXME:this is not working - # if self.state == DeviceConnectionState.DEVICE_NOT_CONNECTED and not Settings.simulation_mode(): - # logging.warning("device not connected") - # return result - - _data = self.cdaq.read_values() - - #fixme: this will not work even if there it will no throw any error - if _data is None: - if not self.cdaq.is_connected(): - logger.warning("CDAQ9184 is disconnected.") - self.update_connection_state(DeviceConnectionState.DEVICE_NOT_CONNECTED) - return result - - ctime = datetime.datetime.strptime(_data[1], '%H:%M:%S.%f') - dtime = ctime - datetime.datetime(1900, 1, 1) - ttime = dtime.total_seconds() - result["daytime"] = ttime - - _data = _data[2:] - - for idx, key in enumerate( - ["ph1_mean", "ph1_dev", "ph2_mean", "ph2_dev", "coll_1_temp_mean", "coll_1_temp_dev", - "coll_2_temp_mean", "coll_2_temp_dev", "sphere_temp_mean", "sphere_temp_dev"]): - result[key] = _data[idx] - return result
        @@ -158,34 +130,6 @@

        Classes

        def get_housekeeping(self) -> dict: result = dict() - result["timestamp"] = format_datetime() - - # FIXME:this is not working - # if self.state == DeviceConnectionState.DEVICE_NOT_CONNECTED and not Settings.simulation_mode(): - # logging.warning("device not connected") - # return result - - _data = self.cdaq.read_values() - - #fixme: this will not work even if there it will no throw any error - if _data is None: - if not self.cdaq.is_connected(): - logger.warning("CDAQ9184 is disconnected.") - self.update_connection_state(DeviceConnectionState.DEVICE_NOT_CONNECTED) - return result - - ctime = datetime.datetime.strptime(_data[1], '%H:%M:%S.%f') - dtime = ctime - datetime.datetime(1900, 1, 1) - ttime = dtime.total_seconds() - result["daytime"] = ttime - - _data = _data[2:] - - for idx, key in enumerate( - ["ph1_mean", "ph1_dev", "ph2_mean", "ph2_dev", "coll_1_temp_mean", "coll_1_temp_dev", - "coll_2_temp_mean", "coll_2_temp_dev", "sphere_temp_mean", "sphere_temp_dev"]): - result[key] = _data[idx] - return result

        Ancestors

        diff --git a/docs/api/egse/powermeter/ni/cdaq9184_ui.html b/docs/api/egse/powermeter/ni/cdaq9184_ui.html index 56c3240..c6371cb 100644 --- a/docs/api/egse/powermeter/ni/cdaq9184_ui.html +++ b/docs/api/egse/powermeter/ni/cdaq9184_ui.html @@ -1122,7 +1122,7 @@

        Methods

        class cdaq9184UIView
        -

        QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

        +

        QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

        Expand source code diff --git a/docs/api/egse/powermeter/thorlabs/pm100a_cs.html b/docs/api/egse/powermeter/thorlabs/pm100a_cs.html index 2beb4d4..e4836ad 100644 --- a/docs/api/egse/powermeter/thorlabs/pm100a_cs.html +++ b/docs/api/egse/powermeter/thorlabs/pm100a_cs.html @@ -350,10 +350,15 @@

        Inherited members

        • ControlServer:
        • diff --git a/docs/api/egse/powermeter/thorlabs/pm100a_ui.html b/docs/api/egse/powermeter/thorlabs/pm100a_ui.html index 0cfe96b..b589130 100644 --- a/docs/api/egse/powermeter/thorlabs/pm100a_ui.html +++ b/docs/api/egse/powermeter/thorlabs/pm100a_ui.html @@ -1572,7 +1572,7 @@

          Methods

          class ThorlabsUIView
          -

          QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

          +

          QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

          Expand source code diff --git a/docs/api/egse/process.html b/docs/api/egse/process.html index 5161b47..67842b6 100644 --- a/docs/api/egse/process.html +++ b/docs/api/egse/process.html @@ -393,7 +393,7 @@

          Module egse.process

          items = [items] if isinstance(items, str) else items - for proc in psutil.process_iter(): + for proc in psutil.process_iter(attrs=['cmdline', 'name'], ad_value='n/a'): with contextlib.suppress(psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): # LOGGER.info(f"{proc.name().lower() = }, {proc.cmdline() = }") if contains: @@ -667,7 +667,7 @@

          Returns

          items = [items] if isinstance(items, str) else items - for proc in psutil.process_iter(): + for proc in psutil.process_iter(attrs=['cmdline', 'name'], ad_value='n/a'): with contextlib.suppress(psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): # LOGGER.info(f"{proc.name().lower() = }, {proc.cmdline() = }") if contains: diff --git a/docs/api/egse/procman/index.html b/docs/api/egse/procman/index.html index 5aad369..ac6a03f 100644 --- a/docs/api/egse/procman/index.html +++ b/docs/api/egse/procman/index.html @@ -50,8 +50,7 @@

          Module egse.procman

          from egse.protocol import CommandProtocol from egse.proxy import Proxy from egse.settings import Settings -from egse.setup import Setup -from egse.state import GlobalState +from egse.setup import Setup, load_setup from egse.storage import is_storage_manager_active from egse.system import find_class from egse.system import format_datetime @@ -406,7 +405,7 @@

          Module egse.procman

          try: - setup = GlobalState.setup + setup = load_setup() devices = {} devices = Setup.find_devices(setup, devices=devices) @@ -509,7 +508,7 @@

          Module egse.procman

          cs.execute(detach_from_parent=True) if process_name == "DAQ6510": - setup = GlobalState.setup + setup = load_setup() das_delay = setup.gse.DAQ6510.route.delay das_count = setup.gse.DAQ6510.route.scan.COUNT.SCAN das_interval = setup.gse.DAQ6510.route.scan.INTERVAL @@ -519,6 +518,18 @@

          Module egse.procman

          str(das_delay)]) das.execute(detach_from_parent="True") + elif process_name == "PTC10": + das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "ptc10", "--auto_regulation"]) + das.execute(detach_from_parent="True") + + elif process_name == "CDAQ OGSE2": + das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "cdaq-photo"]) + das.execute(detach_from_parent="True") + + elif process_name == "CDAQ ALARMS": # TODO + das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "cdaq-alarms"]) + das.execute(detach_from_parent="True") + # Check to see the CS actually started time.sleep(5) @@ -639,6 +650,18 @@

          Module egse.procman

          das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "daq6510"]) das.execute(detach_from_parent="True") + elif process_name == "PTC10": + das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "ptc10", "--auto_regulation"]) + das.execute(detach_from_parent="True") + + elif process_name == "CDAQ OGSE2": + das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "cdaq-photo"]) + das.execute(detach_from_parent="True") + + elif process_name == "CDAQ ALARMS": # TODO + das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "cdaq-alarms"]) + das.execute(detach_from_parent="True") + # Check to see the CS actually started time.sleep(5) @@ -1041,7 +1064,7 @@

          Inherited members

          try: - setup = GlobalState.setup + setup = load_setup() devices = {} devices = Setup.find_devices(setup, devices=devices) @@ -1144,7 +1167,7 @@

          Inherited members

          cs.execute(detach_from_parent=True) if process_name == "DAQ6510": - setup = GlobalState.setup + setup = load_setup() das_delay = setup.gse.DAQ6510.route.delay das_count = setup.gse.DAQ6510.route.scan.COUNT.SCAN das_interval = setup.gse.DAQ6510.route.scan.INTERVAL @@ -1154,6 +1177,18 @@

          Inherited members

          str(das_delay)]) das.execute(detach_from_parent="True") + elif process_name == "PTC10": + das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "ptc10", "--auto_regulation"]) + das.execute(detach_from_parent="True") + + elif process_name == "CDAQ OGSE2": + das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "cdaq-photo"]) + das.execute(detach_from_parent="True") + + elif process_name == "CDAQ ALARMS": # TODO + das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "cdaq-alarms"]) + das.execute(detach_from_parent="True") + # Check to see the CS actually started time.sleep(5) @@ -1274,6 +1309,18 @@

          Inherited members

          das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "daq6510"]) das.execute(detach_from_parent="True") + elif process_name == "PTC10": + das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "ptc10", "--auto_regulation"]) + das.execute(detach_from_parent="True") + + elif process_name == "CDAQ OGSE2": + das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "cdaq-photo"]) + das.execute(detach_from_parent="True") + + elif process_name == "CDAQ ALARMS": # TODO + das = SubProcess("MyApp", [sys.executable, "-m", "egse.das", "cdaq-alarms"]) + das.execute(detach_from_parent="True") + # Check to see the CS actually started time.sleep(5) @@ -1542,7 +1589,7 @@

          Returns

          try: - setup = GlobalState.setup + setup = load_setup() devices = {} devices = Setup.find_devices(setup, devices=devices) diff --git a/docs/api/egse/procman/procman_cs.html b/docs/api/egse/procman/procman_cs.html index efcbcde..2856d69 100644 --- a/docs/api/egse/procman/procman_cs.html +++ b/docs/api/egse/procman/procman_cs.html @@ -559,10 +559,15 @@

          Inherited members

          • ControlServer:
          • diff --git a/docs/api/egse/procman/procman_ui.html b/docs/api/egse/procman/procman_ui.html index fb12ade..b839e32 100644 --- a/docs/api/egse/procman/procman_ui.html +++ b/docs/api/egse/procman/procman_ui.html @@ -253,6 +253,11 @@

            Module egse.procman.procman_ui

            commanding_port = ctrl_settings.CDAQ9184["COMMANDING_PORT"] monitoring_port = ctrl_settings.CDAQ9184["MONITORING_PORT"] + elif module_name == "egse.tempcontrol.lakeshore.lsci": + name = "LS_" + self.process_name.split(" ")[2].upper() + commanding_port = ctrl_settings[name]["COMMANDING_PORT"] + monitoring_port = ctrl_settings[name]["MONITORING_PORT"] + else: commanding_port = ctrl_settings.COMMANDING_PORT monitoring_port = ctrl_settings.MONITORING_PORT @@ -644,9 +649,13 @@

            Module egse.procman.procman_ui

            else: # No GUI defined in the YAML file of the CS LOGGER.debug(f"No GUI available for {self.process_name}") return - - gui_process = SubProcess("MyApp", [sys.executable, "-m", gui_type]) - gui_process.execute(detach_from_parent=True) + if module_name == "egse.tempcontrol.lakeshore.lsci": + index = self.process_name.split(" ")[2] + gui_process = SubProcess("MyApp", [sys.executable, "-m", gui_type, "--index", index]) + gui_process.execute(detach_from_parent=True) + else: + gui_process = SubProcess("MyApp", [sys.executable, "-m", gui_type]) + gui_process.execute(detach_from_parent=True) def set_cs_start_mode(self): """ Change the start mode for the Control Server. @@ -687,7 +696,6 @@

            Module egse.procman.procman_ui

            with ProcessManagerProxy() as pm: response: Response = pm.start_cs(self.process_name, self.is_simulator_mode) - if response.successful: self.sim_option_button.setDisabled(True) else: @@ -2451,7 +2459,7 @@

            Classes

            class ConfigurationMonitoringWorker
            -

            QObject(parent: QObject = None)

            +

            QObject(parent: typing.Optional[QObject] = None)

            Initialisation of a monitoring thread.

            This monitoring thread will listen on the monitoring port of the Control Server. @@ -2742,8 +2750,8 @@

            Class variables

            (parent)
            -

            QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

            +

            QGroupBox(parent: typing.Optional[QWidget] = None) +QGroupBox(title: str, parent: typing.Optional[QWidget] = None)

            Initialisation of a process widget.

            For a Control Server with the given process name and Proxy type, a process widget is created. This widget will @@ -2861,8 +2869,8 @@

            Inherited members

            (parent)
            -

            QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

            +

            QGroupBox(parent: typing.Optional[QWidget] = None) +QGroupBox(title: str, parent: typing.Optional[QWidget] = None)

            Initialisation of a process widget.

            For a Control Server with the given process name and Proxy type, a process widget is created. This widget will @@ -2960,8 +2968,8 @@

            Inherited members

            (parent)
            -

            QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

            +

            QGroupBox(parent: typing.Optional[QWidget] = None) +QGroupBox(title: str, parent: typing.Optional[QWidget] = None)

            Initialisation of a process widget.

            For a Control Server with the given process name and Proxy type, a process widget is created. This widget will @@ -3113,6 +3121,11 @@

            Args

            commanding_port = ctrl_settings.CDAQ9184["COMMANDING_PORT"] monitoring_port = ctrl_settings.CDAQ9184["MONITORING_PORT"] + elif module_name == "egse.tempcontrol.lakeshore.lsci": + name = "LS_" + self.process_name.split(" ")[2].upper() + commanding_port = ctrl_settings[name]["COMMANDING_PORT"] + monitoring_port = ctrl_settings[name]["MONITORING_PORT"] + else: commanding_port = ctrl_settings.COMMANDING_PORT monitoring_port = ctrl_settings.MONITORING_PORT @@ -3343,6 +3356,11 @@

            Methods

            commanding_port = ctrl_settings.CDAQ9184["COMMANDING_PORT"] monitoring_port = ctrl_settings.CDAQ9184["MONITORING_PORT"] + elif module_name == "egse.tempcontrol.lakeshore.lsci": + name = "LS_" + self.process_name.split(" ")[2].upper() + commanding_port = ctrl_settings[name]["COMMANDING_PORT"] + monitoring_port = ctrl_settings[name]["MONITORING_PORT"] + else: commanding_port = ctrl_settings.COMMANDING_PORT monitoring_port = ctrl_settings.MONITORING_PORT @@ -3594,8 +3612,8 @@

            Returns

            (parent)
            -

            QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

            +

            QGroupBox(parent: typing.Optional[QWidget] = None) +QGroupBox(title: str, parent: typing.Optional[QWidget] = None)

            Initialisation of a process widget.

            For a Control Server with the given process name and Proxy type, a process widget is created. This widget will @@ -3693,8 +3711,8 @@

            Inherited members

            (process_name, parent, include_gui_button=True, include_start_stop_button=True)
            -

            QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

            +

            QGroupBox(parent: typing.Optional[QWidget] = None) +QGroupBox(title: str, parent: typing.Optional[QWidget] = None)

            Initialisation of a process widget.

            For a Control Server with the given process name and Proxy type, a process widget is created. This widget will @@ -4020,7 +4038,7 @@

            Returns

            class ProcessManagerUIView
            -

            QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

            +

            QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

            Initialisation of the Process Manager GUI.

            The components are placed in the window and threads are created for all processes, to be able to check for updates in the process status (in the background) and display these in the GUI.

            @@ -6006,8 +6024,8 @@

            Methods

            (process_name, process_info, parent, include_start_stop_button=True)
            -

            QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

            +

            QGroupBox(parent: typing.Optional[QWidget] = None) +QGroupBox(title: str, parent: typing.Optional[QWidget] = None)

            Initialisation of a process widget.

            For a Control Server with the given process name and Proxy type, a process widget is created. This widget will @@ -6162,9 +6180,13 @@

            Args

            else: # No GUI defined in the YAML file of the CS LOGGER.debug(f"No GUI available for {self.process_name}") return - - gui_process = SubProcess("MyApp", [sys.executable, "-m", gui_type]) - gui_process.execute(detach_from_parent=True) + if module_name == "egse.tempcontrol.lakeshore.lsci": + index = self.process_name.split(" ")[2] + gui_process = SubProcess("MyApp", [sys.executable, "-m", gui_type, "--index", index]) + gui_process.execute(detach_from_parent=True) + else: + gui_process = SubProcess("MyApp", [sys.executable, "-m", gui_type]) + gui_process.execute(detach_from_parent=True) def set_cs_start_mode(self): """ Change the start mode for the Control Server. @@ -6205,7 +6227,6 @@

            Args

            with ProcessManagerProxy() as pm: response: Response = pm.start_cs(self.process_name, self.is_simulator_mode) - if response.successful: self.sim_option_button.setDisabled(True) else: @@ -6432,7 +6453,6 @@

            Args

            with ProcessManagerProxy() as pm: response: Response = pm.start_cs(self.process_name, self.is_simulator_mode) - if response.successful: self.sim_option_button.setDisabled(True) else: @@ -6490,9 +6510,13 @@

            Args

            else: # No GUI defined in the YAML file of the CS LOGGER.debug(f"No GUI available for {self.process_name}") return - - gui_process = SubProcess("MyApp", [sys.executable, "-m", gui_type]) - gui_process.execute(detach_from_parent=True) + if module_name == "egse.tempcontrol.lakeshore.lsci": + index = self.process_name.split(" ")[2] + gui_process = SubProcess("MyApp", [sys.executable, "-m", gui_type, "--index", index]) + gui_process.execute(detach_from_parent=True) + else: + gui_process = SubProcess("MyApp", [sys.executable, "-m", gui_type]) + gui_process.execute(detach_from_parent=True)
          diff --git a/docs/api/egse/protocol.html b/docs/api/egse/protocol.html index 7b61ac5..38b2a05 100644 --- a/docs/api/egse/protocol.html +++ b/docs/api/egse/protocol.html @@ -41,6 +41,8 @@

          Module egse.protocol

          command definitions. """ +from __future__ import annotations + import abc import inspect import logging @@ -351,8 +353,9 @@

          Module egse.protocol

          def __init__(self): super().__init__() self.__socket = None - self._commands = dict() # variable is used by sub classes - self._method_lookup = dict() # lookup table for device methods + self._commands = {} # variable is used by sub classes + self.control_server: ControlServer | None = None # variable set by the sub-class + self._method_lookup = {} # lookup table for device methods def bind(self, socket): """Bind to a socket to listen for commands.""" @@ -403,6 +406,8 @@

          Module egse.protocol

          self.send(self.control_server.get_commanding_port()) elif cmd_name == "get_ip_address": self.send(self.control_server.get_ip_address()) + elif cmd_name == "get_storage_mnemonic": + self.send(self.control_server.get_storage_mnemonic()) elif cmd: COMMAND_REQUESTS.labels(target="device").inc() cmd.server_call(self, *args, **kwargs) @@ -573,7 +578,7 @@

          Module egse.protocol

          f"cmd='{cmd}', " f"response={response_method}, device_method={device_method})", ) - logger.debug(f"Creating {command_class.__name__} command with {name=}, {cmd=}, {device_method=}") + # logger.debug(f"Creating {command_class.__name__} command with {name=}, {cmd=}, {device_method=}") self._commands[name] = command_class( name=name, @@ -766,7 +771,7 @@

          Classes

          class BaseCommandProtocol -(control_server: ControlServer) +(control_server: ControlServer)

          An observer for the connection state of a device. Add the sub-class of this class to @@ -1282,8 +1287,9 @@

          Inherited members

          def __init__(self): super().__init__() self.__socket = None - self._commands = dict() # variable is used by sub classes - self._method_lookup = dict() # lookup table for device methods + self._commands = {} # variable is used by sub classes + self.control_server: ControlServer | None = None # variable set by the sub-class + self._method_lookup = {} # lookup table for device methods def bind(self, socket): """Bind to a socket to listen for commands.""" @@ -1334,6 +1340,8 @@

          Inherited members

          self.send(self.control_server.get_commanding_port()) elif cmd_name == "get_ip_address": self.send(self.control_server.get_ip_address()) + elif cmd_name == "get_storage_mnemonic": + self.send(self.control_server.get_storage_mnemonic()) elif cmd: COMMAND_REQUESTS.labels(target="device").inc() cmd.server_call(self, *args, **kwargs) @@ -1504,7 +1512,7 @@

          Inherited members

          f"cmd='{cmd}', " f"response={response_method}, device_method={device_method})", ) - logger.debug(f"Creating {command_class.__name__} command with {name=}, {cmd=}, {device_method=}") + # logger.debug(f"Creating {command_class.__name__} command with {name=}, {cmd=}, {device_method=}") self._commands[name] = command_class( name=name, @@ -1571,14 +1579,13 @@

          Ancestors

          Subclasses

        -def handle_device_method(self, cmd: Command, *args, **kwargs) +def handle_device_method(self, cmd: Command, *args, **kwargs)

        Call the device method with the given arguments.

        @@ -1986,7 +1995,7 @@

        Args

        f"cmd='{cmd}', " f"response={response_method}, device_method={device_method})", ) - logger.debug(f"Creating {command_class.__name__} command with {name=}, {cmd=}, {device_method=}") + # logger.debug(f"Creating {command_class.__name__} command with {name=}, {cmd=}, {device_method=}") self._commands[name] = command_class( name=name, @@ -2091,7 +2100,7 @@

        Inherited members

        class DynamicCommandProtocol -(control_server: ControlServer) +(control_server: ControlServer)

        An observer for the connection state of a device. Add the sub-class of this class to @@ -2157,7 +2166,7 @@

        Subclasses

        Methods

        -def handle_device_method(self, cmd: Command, *args, **kwargs) +def handle_device_method(self, cmd: Command, *args, **kwargs)

        Call the device method with the given arguments.

        diff --git a/docs/api/egse/proxy.html b/docs/api/egse/proxy.html index 128d05d..99e8078 100644 --- a/docs/api/egse/proxy.html +++ b/docs/api/egse/proxy.html @@ -45,6 +45,7 @@

        Module egse.proxy

        import zmq +from egse.control import Failure from egse.decorators import dynamic_interface from egse.mixin import DynamicClientCommandMixin from egse.system import AttributeDict @@ -142,7 +143,7 @@

        Module egse.proxy

        def __enter__(self): if not self.ping(): - raise ConnectionError("Proxy is not connected when entering the context.") + raise ConnectionError(f"Proxy is not connected to endpoint ({self._endpoint}) when entering the context.") return self @@ -442,7 +443,8 @@

        Module egse.proxy

        if retries_left == 0: self._logger.critical(f"Control Server seems to be off-line, abandoning ({data})") - return None + from egse.control import Failure + return Failure(f"Control Server seems to be off-line, abandoning ({data})") retries_left -= 1 self._logger.log(logging.CRITICAL, f"Reconnecting {self.__class__.__name__}, {retries_left=}") @@ -454,7 +456,10 @@

        Module egse.proxy

        self._socket.send(pickle_string) def _request_commands(self): - self._commands = self.send("send_commands") + response = self.send("send_commands") + if isinstance(response, Failure): + raise response + self._commands = response def _add_commands(self): for key in self._commands: @@ -496,11 +501,15 @@

        Module egse.proxy

        behavior of the Proxy command will not be what is expected. """ # bind the client_call method from each Command to this Proxy object - # TODO(rik): what will happen when the _request_commands() fails? if self.is_cs_connected(): - self._request_commands() - self._add_commands() - return True + try: + self._request_commands() + except Failure as exc: + self._logger.warning(f"Failed to request commands for {self.__class__.__name__}: {exc}") + return False + else: + self._add_commands() + return True else: self._logger.warning(f"{self.__class__.__name__} is not connected, try to reconnect.") return False @@ -621,7 +630,7 @@

        Classes

        def __enter__(self): if not self.ping(): - raise ConnectionError("Proxy is not connected when entering the context.") + raise ConnectionError(f"Proxy is not connected to endpoint ({self._endpoint}) when entering the context.") return self @@ -1406,7 +1415,8 @@

        Inherited members

        if retries_left == 0: self._logger.critical(f"Control Server seems to be off-line, abandoning ({data})") - return None + from egse.control import Failure + return Failure(f"Control Server seems to be off-line, abandoning ({data})") retries_left -= 1 self._logger.log(logging.CRITICAL, f"Reconnecting {self.__class__.__name__}, {retries_left=}") @@ -1418,7 +1428,10 @@

        Inherited members

        self._socket.send(pickle_string) def _request_commands(self): - self._commands = self.send("send_commands") + response = self.send("send_commands") + if isinstance(response, Failure): + raise response + self._commands = response def _add_commands(self): for key in self._commands: @@ -1460,11 +1473,15 @@

        Inherited members

        behavior of the Proxy command will not be what is expected. """ # bind the client_call method from each Command to this Proxy object - # TODO(rik): what will happen when the _request_commands() fails? if self.is_cs_connected(): - self._request_commands() - self._add_commands() - return True + try: + self._request_commands() + except Failure as exc: + self._logger.warning(f"Failed to request commands for {self.__class__.__name__}: {exc}") + return False + else: + self._add_commands() + return True else: self._logger.warning(f"{self.__class__.__name__} is not connected, try to reconnect.") return False @@ -1528,7 +1545,7 @@

        Subclasses

      • AWGProxy
      • CRIOProxy
      • PSUProxy
      • -
      • AlertManagerProxy
      • +
      • AlertManagerProxy
      • BeagleboneProxy
      • ConfigurationManagerProxy
      • DPUProxy
      • @@ -1542,6 +1559,7 @@

        Subclasses

      • ZondaProxy
      • BeagleboneProxy
      • LampEQ99Proxy
      • +
      • cdaq9375Proxy
      • cdaq9184Proxy
      • ThorlabsPM100Proxy
      • ProcessManagerProxy
      • @@ -1554,16 +1572,15 @@

        Subclasses

      • SynopticsManagerProxy
      • Agilent34970Proxy
      • Agilent34972Proxy
      • -
      • BeagleboneProxy
      • +
      • DigaloxProxy
      • DAQ6510Proxy
      • -
      • LakeShore336Proxy
      • -
      • PidProxy
      • +
      • LakeShoreProxy
      • ptc10Proxy
      • APCProxy
      • BeagleboneProxy
      • Igm402Proxy
      • Leo3Proxy
      • -
      • VacscanProxy
      • +
      • EvisionProxy
      • Acp40Proxy
      • Tc400Proxy
      • Tpg261Proxy
      • @@ -1657,11 +1674,15 @@

        Returns

        behavior of the Proxy command will not be what is expected. """ # bind the client_call method from each Command to this Proxy object - # TODO(rik): what will happen when the _request_commands() fails? if self.is_cs_connected(): - self._request_commands() - self._add_commands() - return True + try: + self._request_commands() + except Failure as exc: + self._logger.warning(f"Failed to request commands for {self.__class__.__name__}: {exc}") + return False + else: + self._add_commands() + return True else: self._logger.warning(f"{self.__class__.__name__} is not connected, try to reconnect.") return False
        diff --git a/docs/api/egse/randomwalk.html b/docs/api/egse/randomwalk.html index e55fa54..e6d9c80 100644 --- a/docs/api/egse/randomwalk.html +++ b/docs/api/egse/randomwalk.html @@ -30,11 +30,19 @@

        Module egse.randomwalk

        """
         This module implements a random walk that can be used in simulators for devices like temperature sensors or power meter.
         """
        +from __future__ import annotations
        +
         import random
         import sys
         from typing import Optional
         
         
        +# Re-implement based on:
        +#
        +# 1. https://isquared.digital/blog/2020-04-12-random-walk/
        +# 2. https://isquared.digital/blog/2020-04-16-brownian-motion/
        +# 3. https://isquared.digital/blog/2020-05-01-drifted-brownian-motion/
        +
         class RandomWalk:
             """
             This class implements a random walk.
        @@ -57,19 +65,22 @@ 

        Module egse.randomwalk

        By default, the returned values will represent a random walk between -1.0 and 1.0 in a step of 1.0. """ - def __init__(self, start: float = 0.0, boundary: tuple = (-1.0, 1.0), + def __init__(self, start: float = 0.0, boundary: tuple = (-1.0, 1.0), threshold: tuple | float = 0.5, scale: float = 1.0, count: int = 1000, seed: Optional[int] = None): """ Args: start: the start value for the random walk [default = 0.0] - boundary: the lower and upper boundaries [default = (-1.0, 1.0) + boundary: the lower and upper boundaries [default = (-1.0, 1.0)] + threshold: the probability to move up or down [default = 0.5] scale: scale the step size [default = 1.0] count: the number of iterations, when <= 0 count will be basically infinite [default = 1000] seed: seed for the initialization of the random generator [default = None] """ - self._probability = [0.05, 0.95] # the probability to move up or down + self._threshold_up = threshold[1] if isinstance(threshold, tuple) else threshold + self._threshold_down = threshold[0] if isinstance(threshold, tuple) else threshold self._boundary = boundary + self._min_value, self._max_value = boundary self._count = count if count > 0 else sys.maxsize self._start = start self._last = start @@ -85,11 +96,15 @@

        Module egse.randomwalk

        def __next__(self): if self._count > 0: - rr = self._random.random() - down = rr < self._probability[0] and self._last > self._boundary[0] - up = rr > self._probability[1] and self._last < self._boundary[1] - direction = - down + up - value = self._last + direction * self._scale + probability = self._random.random() + if probability > self._threshold_up: + value = self._last + self._scale + value = min(value, self._max_value) + elif probability < self._threshold_down: + value = self._last - self._scale + value = max(value, self._min_value) + else: + value = self._last self._last = value self._count -= 1 return value @@ -98,24 +113,38 @@

        Module egse.randomwalk

        if __name__ == "__main__": + # https://towardsdatascience.com/generating-synthetic-time-series-data-with-random-walks-8701bb9a56a8 + import matplotlib.pyplot as plt # Random Walk, each time a different seed by default - values = [ - value - for value in RandomWalk(start=15.0, boundary=(-100, 25), scale=1, count=10000) - ] - plt.plot(values) + y1 = RandomWalk(start=50.0, boundary=(0, 100), threshold=(0.25, 0.70), scale=1, count=1000) + # y2 = RandomWalk(start=50.0, boundary=(0, 100), threshold=0.49, scale=0.05, count=100000, seed=0) + # y3 = RandomWalk(start=50.0, boundary=(0, 100), threshold=0.48, scale=0.01, count=100000, seed=0) + # plt.plot(list(zip(y1, y2, y3))) + plt.plot(list(y1)) + plt.show() + + y1 = RandomWalk(start=50.0, boundary=(0, 100), threshold=0.5, scale=0.2, count=100000, seed=0) + y2 = RandomWalk(start=50.0, boundary=(0, 100), threshold=0.51, scale=0.2, count=100000, seed=0) + y3 = RandomWalk(start=50.0, boundary=(0, 100), threshold=0.52, scale=0.2, count=100000, seed=0) + plt.plot(list(zip(y1, y2, y3))) + plt.show() + + y1 = RandomWalk(start=75.0, boundary=(0, 100), threshold=0.5, scale=0.2, count=100000, seed=0) + y2 = RandomWalk(start=50.0, boundary=(0, 100), threshold=0.5, scale=0.2, count=100000, seed=0) + y3 = RandomWalk(start=25.0, boundary=(0, 100), threshold=0.5, scale=0.2, count=100000, seed=0) + plt.plot(list(zip(y1, y2, y3))) plt.show() # Three random walks, two of them are fixed and reproducible rng1 = RandomWalk(start=15.0, boundary=(-20, 25), scale=0.8, count=10000, seed=1) - rng2 = RandomWalk(start=15.0, boundary=(-20, 25), scale=0.8, count=10000) - rng3 = RandomWalk(start=15.0, boundary=(-20, 25), scale=0.8, count=10000, seed=0) + rng2 = RandomWalk(start=15.0, boundary=(-20, 25), scale=0.5, count=10000, seed=1) + rng3 = RandomWalk(start=15.0, boundary=(-20, 25), scale=0.3, count=10000, seed=1) - values = [(x, y, z) for x, y, z in zip(rng1, rng2, rng3)] + values = list(zip(rng1, rng2, rng3)) plt.plot(values) plt.ylim(-20, 25) plt.show() @@ -129,13 +158,14 @@

        Module egse.randomwalk

        # Example 2: should give the same output as example 1 - plt.plot([value for value in RandomWalk(start=25.0, boundary=(-100, 100), scale=0.2, - count=1000000, seed=42)]) + plt.plot( + list(RandomWalk(start=25.0, boundary=(-100, 100), scale=0.2, count=1000000, seed=42,)) + ) plt.show() # Example 3: - for i, value in enumerate(RandomWalk(start=25.0, boundary=(-100, 100), scale=2, count=20)): + for i, value in enumerate(RandomWalk(start=25.0, boundary=(-100, 100), threshold=0.5, scale=2, count=20)): print(f"{i}, {value}")
    @@ -150,7 +180,7 @@

    Classes

    class RandomWalk -(start: float = 0.0, boundary: tuple = (-1.0, 1.0), scale: float = 1.0, count: int = 1000, seed: Optional[int] = None) +(start: float = 0.0, boundary: tuple = (-1.0, 1.0), threshold: tuple | float = 0.5, scale: float = 1.0, count: int = 1000, seed: Optional[int] = None)

    This class implements a random walk.

    @@ -170,7 +200,9 @@

    Args

    start
    the start value for the random walk [default = 0.0]
    boundary
    -
    the lower and upper boundaries [default = (-1.0, 1.0)
    +
    the lower and upper boundaries [default = (-1.0, 1.0)]
    +
    threshold
    +
    the probability to move up or down [default = 0.5]
    scale
    scale the step size [default = 1.0]
    count
    @@ -204,19 +236,22 @@

    Args

    By default, the returned values will represent a random walk between -1.0 and 1.0 in a step of 1.0. """ - def __init__(self, start: float = 0.0, boundary: tuple = (-1.0, 1.0), + def __init__(self, start: float = 0.0, boundary: tuple = (-1.0, 1.0), threshold: tuple | float = 0.5, scale: float = 1.0, count: int = 1000, seed: Optional[int] = None): """ Args: start: the start value for the random walk [default = 0.0] - boundary: the lower and upper boundaries [default = (-1.0, 1.0) + boundary: the lower and upper boundaries [default = (-1.0, 1.0)] + threshold: the probability to move up or down [default = 0.5] scale: scale the step size [default = 1.0] count: the number of iterations, when <= 0 count will be basically infinite [default = 1000] seed: seed for the initialization of the random generator [default = None] """ - self._probability = [0.05, 0.95] # the probability to move up or down + self._threshold_up = threshold[1] if isinstance(threshold, tuple) else threshold + self._threshold_down = threshold[0] if isinstance(threshold, tuple) else threshold self._boundary = boundary + self._min_value, self._max_value = boundary self._count = count if count > 0 else sys.maxsize self._start = start self._last = start @@ -232,11 +267,15 @@

    Args

    def __next__(self): if self._count > 0: - rr = self._random.random() - down = rr < self._probability[0] and self._last > self._boundary[0] - up = rr > self._probability[1] and self._last < self._boundary[1] - direction = - down + up - value = self._last + direction * self._scale + probability = self._random.random() + if probability > self._threshold_up: + value = self._last + self._scale + value = min(value, self._max_value) + elif probability < self._threshold_down: + value = self._last - self._scale + value = max(value, self._min_value) + else: + value = self._last self._last = value self._count -= 1 return value diff --git a/docs/api/egse/reg.html b/docs/api/egse/reg.html index b8eeac0..3916c51 100644 --- a/docs/api/egse/reg.html +++ b/docs/api/egse/reg.html @@ -467,10 +467,11 @@

    Module egse.reg

    set_bit(temp, start + bit) if value & (1 << bit) else clear_bit(temp, start + bit) ) self._memory_map[reg.address : reg.address + 4] = temp.to_bytes(4, "big") - LOGGER.debug( - f"set new value for register {reg_name}:{var_name}: " - f"0b{beautify_binary(temp, size=32)} (was 0b{beautify_binary(orig_value, size=32)})" - ) + if temp != orig_value: + LOGGER.debug( + f"set new value for register {reg_name}:{var_name}: " + f"0b{beautify_binary(temp, size=32)} (was 0b{beautify_binary(orig_value, size=32)})" + ) def add_register(self, reg: Register): """ @@ -1153,10 +1154,11 @@

    Args

    set_bit(temp, start + bit) if value & (1 << bit) else clear_bit(temp, start + bit) ) self._memory_map[reg.address : reg.address + 4] = temp.to_bytes(4, "big") - LOGGER.debug( - f"set new value for register {reg_name}:{var_name}: " - f"0b{beautify_binary(temp, size=32)} (was 0b{beautify_binary(orig_value, size=32)})" - ) + if temp != orig_value: + LOGGER.debug( + f"set new value for register {reg_name}:{var_name}: " + f"0b{beautify_binary(temp, size=32)} (was 0b{beautify_binary(orig_value, size=32)})" + ) def add_register(self, reg: Register): """ @@ -1668,10 +1670,11 @@

    Raises

    set_bit(temp, start + bit) if value & (1 << bit) else clear_bit(temp, start + bit) ) self._memory_map[reg.address : reg.address + 4] = temp.to_bytes(4, "big") - LOGGER.debug( - f"set new value for register {reg_name}:{var_name}: " - f"0b{beautify_binary(temp, size=32)} (was 0b{beautify_binary(orig_value, size=32)})" - ) + if temp != orig_value: + LOGGER.debug( + f"set new value for register {reg_name}:{var_name}: " + f"0b{beautify_binary(temp, size=32)} (was 0b{beautify_binary(orig_value, size=32)})" + )
    diff --git a/docs/api/egse/reprocess.html b/docs/api/egse/reprocess.html index 62b220c..51147d2 100644 --- a/docs/api/egse/reprocess.html +++ b/docs/api/egse/reprocess.html @@ -39,16 +39,22 @@

    Module egse.reprocess

    from egse import h5 from egse.config import find_file -from egse.dpu.fitsgen import get_hdf5_filenames_for_obsid, get_od, create_fits_from_hdf5, add_synoptics +from egse.dpu.fitsgen import add_synoptics +from egse.dpu.fitsgen import create_fits_from_hdf5 +from egse.dpu.fitsgen import get_hdf5_filenames_for_obsid +from egse.dpu.fitsgen import get_od from egse.fee.n_fee_hk import ORIGIN as N_FEE_ORIGIN from egse.fee.n_fee_hk import ORIGIN as ORIGIN_NFEE_HK -from egse.fee.n_fee_hk import get_calibrated_supply_voltages, get_calibrated_temperatures +from egse.fee.n_fee_hk import get_calibrated_supply_voltages +from egse.fee.n_fee_hk import get_calibrated_temperatures from egse.fee.nfee import HousekeepingData from egse.fov import ORIGIN as FOV_ORIGIN from egse.hk import read_conversion_dict -from egse.obsid import ObservationIdentifier, obsid_from_storage +from egse.obsid import ObservationIdentifier +from egse.obsid import obsid_from_storage from egse.settings import Settings -from egse.setup import NavigableDict, Setup +from egse.setup import NavigableDict +from egse.setup import Setup from egse.spw import SpaceWirePacket from egse.storage import HDF5 from egse.storage.persistence import CSV @@ -113,8 +119,10 @@

    Module egse.reprocess

    fee_calibration = setup.camera.fee.calibration - reprocess_daily_n_fee_hk(od, site, fee_calibration, archive_dir, reprocessed_archive_dir) # N-FEE HK - reprocess_daily_synoptics(od, site, archive_dir, fixed_archive_dir, reprocessed_archive_dir) # Synoptics + # Order is important here: Synoptics builds further on N-FEE HK + reprocess_daily_n_fee_hk(od, site, fee_calibration, archive_dir, reprocessed_archive_dir, setup) # N-FEE HK + reprocess_daily_synoptics(od, site, archive_dir, fixed_archive_dir, reprocessed_archive_dir, setup) # Synoptics + def reprocess_obsid(obsid: Union[str, int, ObservationIdentifier], site: str, setup: Setup, archive_dir: str, @@ -138,14 +146,15 @@

    Module egse.reprocess

    fee_calibration = setup.camera.fee.calibration - reprocess_obs_n_fee_hk(obsid, fee_calibration, archive_dir, reprocessed_archive_dir) # N-FEE HK - reprocess_obs_synoptics(obsid, site, archive_dir, fixed_archive_dir, reprocessed_archive_dir) # Synoptics + # Order is important here: Synoptics builds further on N-FEE HK + reprocess_obs_n_fee_hk(obsid, fee_calibration, archive_dir, reprocessed_archive_dir, setup) # N-FEE HK + reprocess_obs_synoptics(obsid, site, archive_dir, fixed_archive_dir, reprocessed_archive_dir, setup) # Synoptics reprocess_ccd_data(obsid, archive_dir, reprocessed_archive_dir, setup) # FITS files def reprocess_daily_n_fee_hk(od: str, site: str, fee_calibration: NavigableDict, archive_dir: str, - reprocessed_archive_dir: str): + reprocessed_archive_dir: str, setup: Setup): """ Re-processing the N-FEE HK for the given OD. The procedure is as follows: @@ -166,6 +175,7 @@

    Module egse.reprocess

    - archive_dir: Folder (with sub-folders /daily and /obs) in which the HDF5 files are stored. - reprocessed_archive_dir: Folder (with sub-folders /daily and /obs) in which the re-processed N-FEE HK files will be stored. + - setup: Setup. """ archive_od_dir = f"{archive_dir}/daily/{od}" @@ -184,11 +194,11 @@

    Module egse.reprocess

    n_fee_hk_filename = f"{reprocessed_archive_od_dir}/{od}_{site}_{N_FEE_ORIGIN}.{CSV.extension}" - _reprocess_n_fee_hk(hdf5_filenames, n_fee_hk_filename, fee_calibration, reprocessed_archive_od_dir) + _reprocess_n_fee_hk(hdf5_filenames, n_fee_hk_filename, fee_calibration, setup) def reprocess_obs_n_fee_hk(obsid: Union[str, int, ObservationIdentifier], fee_calibration: NavigableDict, - archive_dir: str, reprocessed_archive_dir: str): + archive_dir: str, reprocessed_archive_dir: str, setup: Setup): """ Re-processing the synoptics for the given obsid. The procedure is as follows: @@ -209,6 +219,7 @@

    Module egse.reprocess

    - archive_dir: Folder (with sub-folders /daily and /obs) in which the HDF5 files are stored. - reprocessed_archive_dir: Folder (with sub-folders /daily and /obs) in which the re-processed N-FEE HK files will be stored. + - setup: Setup. """ obsid = obsid_from_storage(obsid, archive_dir) @@ -235,10 +246,10 @@

    Module egse.reprocess

    else: n_fee_hk_filename = f"{reprocessed_archive_obsid_dir}/{n_fee_hk_filename.stem}.{CSV.extension}" - _reprocess_n_fee_hk(hdf5_filenames, n_fee_hk_filename, fee_calibration, reprocessed_archive_obsid_dir) + _reprocess_n_fee_hk(hdf5_filenames, n_fee_hk_filename, fee_calibration, setup) -def _reprocess_n_fee_hk(hdf5_filenames, n_fee_hk_filename: str, fee_calibration: NavigableDict): +def _reprocess_n_fee_hk(hdf5_filenames, n_fee_hk_filename: str, fee_calibration: NavigableDict, setup: Setup): """ Re-processing the N-FEE HK. The procedure is as follows for the "hk" dataset in all groups of the given list of HDF5 files: @@ -251,9 +262,10 @@

    Module egse.reprocess

    - hdf5_filenames: Filenames of the HDF5 files containing the raw N-FEE HK. - n_fee_hk_filename: Filename for the re-processed N-FEE HK file. - fee_calibration: Calibration data for the N-FEE HK. + - setup: Setup. """ - hk_names_mapping = read_conversion_dict(ORIGIN_NFEE_HK, use_site=False) + hk_names_mapping = read_conversion_dict(ORIGIN_NFEE_HK, use_site=False, setup=setup) supply_voltage_calibration = fee_calibration.supply_voltages temperature_calibration = fee_calibration.temperatures @@ -295,7 +307,7 @@

    Module egse.reprocess

    calibrated_supply_voltages = get_calibrated_supply_voltages(values, supply_voltage_calibration) values.update(calibrated_supply_voltages) - calibrated_temperatures = get_calibrated_temperatures(values, temperature_calibration) + calibrated_temperatures = get_calibrated_temperatures(values, temperature_calibration, setup) values.update(calibrated_temperatures) # Store the new N-FEE HK @@ -306,7 +318,7 @@

    Module egse.reprocess

    def reprocess_daily_synoptics(od: str, site: str, archive_dir: str, fixed_archive_dir: str, - reprocessed_archive_dir: str): + reprocessed_archive_dir: str, setup: Setup): """ Re-processing the N-FEE HK for the given OD. The procedure is as follows: @@ -322,6 +334,7 @@

    Module egse.reprocess

    - fixed_archive_dir: Folder (with sub-folders /daily and /obs) in which the fixed HK files are stored. - reprocessed_archive_dir: Folder (with sub-folders /daily and /obs) in which the re-processed N-FEE HK files will be stored. + - setup: Setup. """ archive_day_dir = f"{archive_dir}/daily/{od}" @@ -340,11 +353,11 @@

    Module egse.reprocess

    return synoptics_filename = f"{reprocessed_archive_day_dir}/{od}_{site}_{SYN_ORIGIN}.{CSV.extension}" - _reprocess_synoptics(syn_input_filenames, synoptics_filename) + _reprocess_synoptics(syn_input_filenames, synoptics_filename, setup) def reprocess_obs_synoptics(obsid: Union[str, int, ObservationIdentifier], site: str, archive_dir: str, - fixed_archive_dir: str, reprocessed_archive_dir: str): + fixed_archive_dir: str, reprocessed_archive_dir: str, setup: Setup): """ Re-processing the N-FEE HK for the given obsid. The procedure is as follows: @@ -360,6 +373,7 @@

    Module egse.reprocess

    - fixed_archive_dir: Folder (with sub-folders /daily and /obs) in which the fixed HK files are stored. - reprocessed_archive_dir: Folder (with sub-folders /daily and /obs) in which the re-processed synoptics files will be stored. + - setup: Setup. """ obsid = obsid_from_storage(obsid, archive_dir) @@ -391,10 +405,10 @@

    Module egse.reprocess

    else: synoptics_filename = f"{reprocessed_archive_obs_dir}/{synoptics_filename.stem}.{CSV.extension}" - _reprocess_synoptics(syn_input_filenames, synoptics_filename) + _reprocess_synoptics(syn_input_filenames, synoptics_filename, setup) -def _reprocess_synoptics(syn_input_filenames, synoptics_filename: str): +def _reprocess_synoptics(syn_input_filenames, synoptics_filename: str, setup: Setup): """ Re-processing the synoptics. The procedure is as follows: @@ -406,9 +420,10 @@

    Module egse.reprocess

    Args: - syn_input_filenames: Filenames of the HK files contributing to the synoptics. - synoptics_filename: Filename for the re-processed synoptics file. + - setup: Setup. """ - syn_names, original_name_egse, original_name_th = read_syn_info() + syn_names, original_name_egse, original_name_th = read_syn_info(setup) # Concatenate the content of all contributing HK files @@ -700,7 +715,10 @@

    Module egse.reprocess

    hdf5_filenames = get_hdf5_filenames_for_obsid(obsid, data_dir=archive_dir) create_fits_from_hdf5(hdf5_filenames, location=reprocessed_archive_dir, setup=setup) - add_synoptics(obsid, data_dir=reprocessed_archive_dir) + + fee_side = setup.camera.fee.ccd_sides.enum + + add_synoptics(obsid, data_dir=reprocessed_archive_dir, fee_side=fee_side)
    @@ -1051,11 +1069,14 @@

    Args

    hdf5_filenames = get_hdf5_filenames_for_obsid(obsid, data_dir=archive_dir) create_fits_from_hdf5(hdf5_filenames, location=reprocessed_archive_dir, setup=setup) - add_synoptics(obsid, data_dir=reprocessed_archive_dir) + + fee_side = setup.camera.fee.ccd_sides.enum + + add_synoptics(obsid, data_dir=reprocessed_archive_dir, fee_side=fee_side)
    -def reprocess_daily_n_fee_hk(od: str, site: str, fee_calibration: egse.setup.NavigableDict, archive_dir: str, reprocessed_archive_dir: str) +def reprocess_daily_n_fee_hk(od: str, site: str, fee_calibration: egse.setup.NavigableDict, archive_dir: str, reprocessed_archive_dir: str, setup: Setup)

    Re-processing the N-FEE HK for the given OD.

    @@ -1080,13 +1101,14 @@

    Args

  • archive_dir: Folder (with sub-folders /daily and /obs) in which the HDF5 files are stored.
  • reprocessed_archive_dir: Folder (with sub-folders /daily and /obs) in which the re-processed N-FEE HK files will be stored.
  • +
  • setup: Setup.
  • Expand source code
    def reprocess_daily_n_fee_hk(od: str, site: str, fee_calibration: NavigableDict, archive_dir: str,
    -                             reprocessed_archive_dir: str):
    +                             reprocessed_archive_dir: str, setup: Setup):
         """ Re-processing the N-FEE HK for the given OD.
     
         The procedure is as follows:
    @@ -1107,6 +1129,7 @@ 

    Args

    - archive_dir: Folder (with sub-folders /daily and /obs) in which the HDF5 files are stored. - reprocessed_archive_dir: Folder (with sub-folders /daily and /obs) in which the re-processed N-FEE HK files will be stored. + - setup: Setup. """ archive_od_dir = f"{archive_dir}/daily/{od}" @@ -1125,11 +1148,11 @@

    Args

    n_fee_hk_filename = f"{reprocessed_archive_od_dir}/{od}_{site}_{N_FEE_ORIGIN}.{CSV.extension}" - _reprocess_n_fee_hk(hdf5_filenames, n_fee_hk_filename, fee_calibration, reprocessed_archive_od_dir)
    + _reprocess_n_fee_hk(hdf5_filenames, n_fee_hk_filename, fee_calibration, setup)
    -def reprocess_daily_synoptics(od: str, site: str, archive_dir: str, fixed_archive_dir: str, reprocessed_archive_dir: str) +def reprocess_daily_synoptics(od: str, site: str, archive_dir: str, fixed_archive_dir: str, reprocessed_archive_dir: str, setup: Setup)

    Re-processing the N-FEE HK for the given OD.

    @@ -1146,13 +1169,14 @@

    Args

  • fixed_archive_dir: Folder (with sub-folders /daily and /obs) in which the fixed HK files are stored.
  • reprocessed_archive_dir: Folder (with sub-folders /daily and /obs) in which the re-processed N-FEE HK files will be stored.
  • +
  • setup: Setup.
  • Expand source code
    def reprocess_daily_synoptics(od: str, site: str, archive_dir: str, fixed_archive_dir: str,
    -                              reprocessed_archive_dir: str):
    +                              reprocessed_archive_dir: str, setup: Setup):
         """ Re-processing the N-FEE HK for the given OD.
     
         The procedure is as follows:
    @@ -1168,6 +1192,7 @@ 

    Args

    - fixed_archive_dir: Folder (with sub-folders /daily and /obs) in which the fixed HK files are stored. - reprocessed_archive_dir: Folder (with sub-folders /daily and /obs) in which the re-processed N-FEE HK files will be stored. + - setup: Setup. """ archive_day_dir = f"{archive_dir}/daily/{od}" @@ -1186,11 +1211,11 @@

    Args

    return synoptics_filename = f"{reprocessed_archive_day_dir}/{od}_{site}_{SYN_ORIGIN}.{CSV.extension}" - _reprocess_synoptics(syn_input_filenames, synoptics_filename)
    + _reprocess_synoptics(syn_input_filenames, synoptics_filename, setup)
    -def reprocess_obs_n_fee_hk(obsid: Union[ObservationIdentifier, str, int], fee_calibration: egse.setup.NavigableDict, archive_dir: str, reprocessed_archive_dir: str) +def reprocess_obs_n_fee_hk(obsid: Union[ObservationIdentifier, str, int], fee_calibration: egse.setup.NavigableDict, archive_dir: str, reprocessed_archive_dir: str, setup: Setup)

    Re-processing the synoptics for the given obsid.

    @@ -1215,13 +1240,14 @@

    Args

  • archive_dir: Folder (with sub-folders /daily and /obs) in which the HDF5 files are stored.
  • reprocessed_archive_dir: Folder (with sub-folders /daily and /obs) in which the re-processed N-FEE HK files will be stored.
  • +
  • setup: Setup.
  • Expand source code
    def reprocess_obs_n_fee_hk(obsid: Union[str, int, ObservationIdentifier], fee_calibration: NavigableDict,
    -                           archive_dir: str, reprocessed_archive_dir: str):
    +                           archive_dir: str, reprocessed_archive_dir: str, setup: Setup):
         """ Re-processing the synoptics for the given obsid.
     
         The procedure is as follows:
    @@ -1242,6 +1268,7 @@ 

    Args

    - archive_dir: Folder (with sub-folders /daily and /obs) in which the HDF5 files are stored. - reprocessed_archive_dir: Folder (with sub-folders /daily and /obs) in which the re-processed N-FEE HK files will be stored. + - setup: Setup. """ obsid = obsid_from_storage(obsid, archive_dir) @@ -1268,11 +1295,11 @@

    Args

    else: n_fee_hk_filename = f"{reprocessed_archive_obsid_dir}/{n_fee_hk_filename.stem}.{CSV.extension}" - _reprocess_n_fee_hk(hdf5_filenames, n_fee_hk_filename, fee_calibration, reprocessed_archive_obsid_dir)
    + _reprocess_n_fee_hk(hdf5_filenames, n_fee_hk_filename, fee_calibration, setup)
    -def reprocess_obs_synoptics(obsid: Union[ObservationIdentifier, str, int], site: str, archive_dir: str, fixed_archive_dir: str, reprocessed_archive_dir: str) +def reprocess_obs_synoptics(obsid: Union[ObservationIdentifier, str, int], site: str, archive_dir: str, fixed_archive_dir: str, reprocessed_archive_dir: str, setup: Setup)

    Re-processing the N-FEE HK for the given obsid.

    @@ -1289,13 +1316,14 @@

    Args

  • fixed_archive_dir: Folder (with sub-folders /daily and /obs) in which the fixed HK files are stored.
  • reprocessed_archive_dir: Folder (with sub-folders /daily and /obs) in which the re-processed synoptics files will be stored.
  • +
  • setup: Setup.
  • Expand source code
    def reprocess_obs_synoptics(obsid: Union[str, int, ObservationIdentifier], site: str, archive_dir: str,
    -                            fixed_archive_dir: str, reprocessed_archive_dir: str):
    +                            fixed_archive_dir: str, reprocessed_archive_dir: str, setup: Setup):
         """ Re-processing the N-FEE HK for the given obsid.
     
         The procedure is as follows:
    @@ -1311,6 +1339,7 @@ 

    Args

    - fixed_archive_dir: Folder (with sub-folders /daily and /obs) in which the fixed HK files are stored. - reprocessed_archive_dir: Folder (with sub-folders /daily and /obs) in which the re-processed synoptics files will be stored. + - setup: Setup. """ obsid = obsid_from_storage(obsid, archive_dir) @@ -1342,7 +1371,7 @@

    Args

    else: synoptics_filename = f"{reprocessed_archive_obs_dir}/{synoptics_filename.stem}.{CSV.extension}" - _reprocess_synoptics(syn_input_filenames, synoptics_filename)
    + _reprocess_synoptics(syn_input_filenames, synoptics_filename, setup)
    @@ -1389,8 +1418,9 @@

    Args

    fee_calibration = setup.camera.fee.calibration - reprocess_obs_n_fee_hk(obsid, fee_calibration, archive_dir, reprocessed_archive_dir) # N-FEE HK - reprocess_obs_synoptics(obsid, site, archive_dir, fixed_archive_dir, reprocessed_archive_dir) # Synoptics + # Order is important here: Synoptics builds further on N-FEE HK + reprocess_obs_n_fee_hk(obsid, fee_calibration, archive_dir, reprocessed_archive_dir, setup) # N-FEE HK + reprocess_obs_synoptics(obsid, site, archive_dir, fixed_archive_dir, reprocessed_archive_dir, setup) # Synoptics reprocess_ccd_data(obsid, archive_dir, reprocessed_archive_dir, setup) # FITS files
    @@ -1437,8 +1467,9 @@

    Args

    fee_calibration = setup.camera.fee.calibration - reprocess_daily_n_fee_hk(od, site, fee_calibration, archive_dir, reprocessed_archive_dir) # N-FEE HK - reprocess_daily_synoptics(od, site, archive_dir, fixed_archive_dir, reprocessed_archive_dir) # Synoptics + # Order is important here: Synoptics builds further on N-FEE HK + reprocess_daily_n_fee_hk(od, site, fee_calibration, archive_dir, reprocessed_archive_dir, setup) # N-FEE HK + reprocess_daily_synoptics(od, site, archive_dir, fixed_archive_dir, reprocessed_archive_dir, setup) # Synoptics
    diff --git a/docs/api/egse/resource.html b/docs/api/egse/resource.html index 04a4a5a..4412335 100644 --- a/docs/api/egse/resource.html +++ b/docs/api/egse/resource.html @@ -620,7 +620,7 @@

    Raises

    -def initialise_resources(root: Union[str, pathlib.Path] = PosixPath('/Users/rik/git/plato-common-egse/src/egse')) +def initialise_resources(root: Union[str, pathlib.Path] = PosixPath('/Users/rik/Documents/PyCharmProjects/plato-common-egse/src/egse'))

    Initialise the default resources.

    diff --git a/docs/api/egse/rmap.html b/docs/api/egse/rmap.html new file mode 100644 index 0000000..e5583f9 --- /dev/null +++ b/docs/api/egse/rmap.html @@ -0,0 +1,1027 @@ + + + + + + +egse.rmap API documentation + + + + + + + + + + + +
    +
    +
    +

    Module egse.rmap

    +
    +
    +
    + +Expand source code + +
    import logging
    +
    +from egse.bits import crc_calc
    +from egse.decorators import static_vars
    +from egse.exceptions import Error
    +
    +# RMAP Error Codes and Constants -------------------------------------------------------------------
    +
    +RMAP_PROTOCOL_ID = 0x01
    +RMAP_TARGET_LOGICAL_ADDRESS_DEFAULT = 0xFE
    +RMAP_TARGET_KEY = 0xD1
    +
    +# Error and Status Codes
    +
    +RMAP_SUCCESS = 0
    +RMAP_GENERAL_ERROR = 1
    +RMAP_UNUSED_PACKET_TYPE_COMMAND_CODE = 2
    +RMAP_INVALID_KEY = 3
    +RMAP_INVALID_DATA_CRC = 4
    +RMAP_EARLY_EOP = 5
    +RMAP_TOO_MUCH_DATA = 6
    +RMAP_EEP = 7
    +RMAP_RESERVED = 8
    +RMAP_VERIFY_BUFFER_OVERRUN = 9
    +RMAP_NOT_IMPLEMENTED_AUTHORISED = 10
    +RMAP_RMW_DATA_LENGTH_ERROR = 11
    +RMAP_INVALID_TARGET_LOGICAL_ADDRESS = 12
    +
    +# Memory Map layout --------------------------------------------------------------------------------
    +
    +# NOTE: These memory areas are currently equal for N-FEE and F-FEE. Don't know if this will
    +#       change in the future.
    +
    +CRITICAL_AREA_START = 0x0000_0000
    +CRITICAL_AREA_END = 0x0000_00FC
    +GENERAL_AREA_START = 0x0000_0100
    +GENERAL_AREA_END = 0x0000_06FC
    +HK_AREA_START = 0x0000_0700
    +HK_AREA_END = 0x0000_07FC
    +WINDOWING_AREA_START = 0x0080_0000
    +WINDOWING_AREA_END = 0x00FF_FFFC
    +
    +
    +class RMAPError(Error):
    +    """An RMAP specific Error."""
    +    pass
    +
    +
    +_LOGGER = logging.getLogger(__name__)
    +
    +
    +def create_rmap_read_request_packet(address: int, length: int, tid: int, strict: bool = True) -> bytes:
    +    """
    +    Creates an RMAP Read Request SpaceWire packet.
    +
    +    The read request is an RMAP command that read a number of bytes from the FEE register memory.
    +
    +    The function returns a ``ctypes`` character array (which is basically a bytes array) that
    +    can be passed into the EtherSpaceLink library function ``esl_write_packet()``.
    +
    +    Address shall be within the 0x0000_0000 and 0x00FF_FFFC. The memory map (register) is divided
    +    in the following areas:
    +
    +        0x0000_0000 - 0x0000_00FC   Critical Configuration Area (verified write)
    +        0x0000_0100 - 0x0000_06FC   General Configuration Area (unverified write)
    +        0x0000_0700 - 0x0000_07FC   Housekeeping area
    +        0x0000_0800 - 0x007F_FFFC   Not Supported
    +        0x0080_0000 - 0x00FF_FFFC   Windowing Area (unverified write)
    +        0x0010_0000 - 0xFFFF_FFFC   Not Supported
    +
    +    All read requests to the critical area shall have a fixed data length of 4 bytes.
    +    All read requests to a general area shall have a maximum data length of 256 bytes.
    +    All read requests to the housekeeping area shall have a maximum data length of 256 bytes.
    +    All read requests to the windowing area shall have a maximum data length of 4096 bytes.
    +
    +    The transaction identifier shall be incremented for each read request. This shall be done by
    +    the calling function!
    +
    +    Args:
    +        address (int): the FEE register memory address
    +        length (int): the data length
    +        tid (int): transaction identifier
    +        strict (bool): perform strict checking of address and length
    +
    +    Returns:
    +        a bytes object containing the full RMAP Read Request packet.
    +    """
    +    from egse.spw import update_transaction_identifier
    +
    +    check_address_and_data_length(address, length, strict=strict)
    +
    +    tid = update_transaction_identifier(tid)
    +
    +    buf = bytearray(16)
    +
    +    # NOTE: The first bytes would each carry the target SpW address or a destination port,
    +    #       but this is not used for point-to-point connections, so we're safe.
    +
    +    buf[0] = 0x51  # Target N-FEE or F-FEE
    +    buf[1] = 0x01  # RMAP Protocol ID
    +    buf[2] = 0x4C  # Instruction: 0b1001100, RMAP Request, Read, Incrementing address, reply address
    +    buf[3] = 0xD1  # Destination Key
    +    buf[4] = 0x50  # Initiator is always the DPU
    +    buf[5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
    +    buf[6] = tid & 0xFF  # LSB of the Transition ID
    +    buf[7] = 0x00  # Extended address is not used
    +    buf[8] = (address >> 24) & 0xFF  # address (MSB)
    +    buf[9] = (address >> 16) & 0xFF  # address
    +    buf[10] = (address >> 8) & 0xFF  # address
    +    buf[11] = address & 0xFF  # address (LSB)
    +    buf[12] = (length >> 16) & 0xFF  # data length (MSB)
    +    buf[13] = (length >> 8) & 0xFF  # data length
    +    buf[14] = length & 0xFF  # data length (LSB)
    +    buf[15] = rmap_crc_check(buf, 0, 15) & 0xFF
    +
    +    return bytes(buf)
    +
    +
    +def create_rmap_read_request_reply_packet(
    +        instruction_field: int, tid: int, status: int, buffer: bytes, buffer_length: int) -> bytes:
    +    """
    +    Creates an RMAP Reply to a RMAP Read Request packet.
    +
    +    The function returns a ``ctypes`` character array (which is basically a bytes array) that
    +    can be passed into the EtherSpaceLink library function ``esl_write_packet()``.
    +
    +    Args:
    +        instruction_field (int): the instruction field of the RMAP read request packet
    +        tid (int): the transaction identifier of the read request packet
    +        status (int): shall be 0 if the read request was successful, contain an error code otherwise.
    +        buffer (bytes): the data that was read as indicated by the read request
    +        buffer_length (int): the data length
    +
    +    Returns:
    +        packet: a bytes object containing the full RMAP Reply packet.
    +    """
    +
    +    buf = bytearray(12 + buffer_length + 1)
    +
    +    buf[0] = 0x50  # Initiator address N-DPU or F-DPU
    +    buf[1] = 0x01  # RMAP Protocol ID
    +    buf[2] = instruction_field & 0x3F  # Clear the command bit as this is a reply
    +    buf[3] = status & 0xFF  # Status field: 0 on success
    +    buf[4] = 0x51  # Target address is always the N-FEE or F-FEE
    +    buf[5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
    +    buf[6] = tid & 0xFF  # LSB of the Transition ID
    +    buf[7] = 0x00  # Reserved
    +    buf[8] = (buffer_length >> 16) & 0xFF  # data length (MSB)
    +    buf[9] = (buffer_length >> 8) & 0xFF  # data length
    +    buf[10] = buffer_length & 0xFF  # data length (LSB)
    +    buf[11] = rmap_crc_check(buf, 0, 11) & 0xFF  # Header CRC
    +
    +    # Note that we assume here that len(buffer) == buffer_length.
    +
    +    if len(buffer) != buffer_length:
    +        _LOGGER.warning(
    +            f"While creating an RMAP read reply packet, the length of the buffer ({len(buffer)}) "
    +            f"not equals the buffer_length ({buffer_length})"
    +        )
    +
    +    for idx, value in enumerate(buffer):
    +        buf[12 + idx] = value
    +
    +    buf[12 + buffer_length] = rmap_crc_check(buffer, 0, buffer_length) & 0xFF  # data CRC
    +
    +    return bytes(buf)
    +
    +
    +def create_rmap_verified_write_packet(address: int, data: bytes, tid: int) -> bytes:
    +    """
    +    Create an RMAP packet for a verified write request on the FEE. The length of the data is
    +    by convention always 4 bytes and therefore not passed as an argument.
    +
    +    Args:
    +        address: the start memory address on the FEE register map
    +        data: the data to be written in the register map at address [4 bytes]
    +        tid (int): transaction identifier
    +
    +    Returns:
    +        packet: a bytes object containing the SpaceWire packet.
    +    """
    +    from egse.spw import update_transaction_identifier
    +
    +    if len(data) < 4:
    +        raise ValueError(
    +            f"The data argument should be at least 4 bytes, but it is only {len(data)} bytes: {data=}.")
    +
    +    if address > CRITICAL_AREA_END:
    +        raise ValueError("The address range for critical configuration is [0x00 - 0xFC].")
    +
    +    tid = update_transaction_identifier(tid)
    +
    +    # Buffer length is fixed at 24 bytes since the data length is fixed
    +    # at 4 bytes (32 bit addressing)
    +
    +    buf = bytearray(21)
    +
    +    # The values below are taken from the PLATO N-FEE to N-DPU
    +    # Interface Requirements Document [PLATO-DLR-PL-ICD-0010]
    +
    +    buf[0] = 0x51  # Logical Address
    +    buf[1] = 0x01  # Protocol ID
    +    buf[2] = 0x7C  # Instruction
    +    buf[3] = 0xD1  # Key
    +    buf[4] = 0x50  # Initiator Address
    +    buf[5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
    +    buf[6] = tid & 0xFF  # LSB of the Transition ID
    +    buf[7] = 0x00  # Extended address
    +    buf[8] = (address >> 24) & 0xFF  # address (MSB)
    +    buf[9] = (address >> 16) & 0xFF  # address
    +    buf[10] = (address >> 8) & 0xFF  # address
    +    buf[11] = address & 0xFF  # address (LSB)
    +    buf[12] = 0x00  # data length (MSB)
    +    buf[13] = 0x00  # data length
    +    buf[14] = 0x04  # data length (LSB)
    +    buf[15] = rmap_crc_check(buf, 0, 15) & 0xFF  # header CRC
    +    buf[16] = data[0]
    +    buf[17] = data[1]
    +    buf[18] = data[2]
    +    buf[19] = data[3]
    +    buf[20] = rmap_crc_check(buf, 16, 4) & 0xFF  # data CRC
    +
    +    return bytes(buf)
    +
    +
    +def create_rmap_unverified_write_packet(address: int, data: bytes, length: int, tid: int) -> bytes:
    +    """
    +    Create an RMAP packet for a unverified write request on the FEE.
    +
    +    Args:
    +        address: the start memory address on the FEE register map
    +        data: the data to be written in the register map at address
    +        length: the length of the data
    +        tid (int): transaction identifier
    +
    +    Returns:
    +        packet: a bytes object containing the SpaceWire packet.
    +    """
    +    from egse.spw import update_transaction_identifier
    +
    +    # We can only handle data for which the length >= the given length argument.
    +
    +    if len(data) < length:
    +        raise ValueError(
    +            f"The length of the data argument ({len(data)}) is smaller than "
    +            f"the given length argument ({length})."
    +        )
    +
    +    if len(data) > length:
    +        _LOGGER.warning(
    +            f"The length of the data argument ({len(data)}) is larger than "
    +            f"the given length argument ({length}). The data will be truncated "
    +            f"when copied into the packet."
    +        )
    +
    +    if address <= CRITICAL_AREA_END:
    +        raise ValueError(
    +            f"The given address (0x{address:08X}) is in the range for critical configuration is "
    +            f"[0x00 - 0xFC]. Use the verified write function for this."
    +        )
    +
    +    tid = update_transaction_identifier(tid)
    +
    +    # Buffer length is fixed at 24 bytes since the data length
    +    # is fixed at 4 bytes (32 bit addressing)
    +
    +    buf = bytearray(16 + length + 1)
    +    offset = 0
    +
    +    buf[offset + 0] = 0x51  # Logical Address
    +    buf[offset + 1] = 0x01  # Protocol ID
    +    buf[offset + 2] = 0x6C  # Instruction
    +    buf[offset + 3] = 0xD1  # Key
    +    buf[offset + 4] = 0x50  # Initiator Address
    +    buf[offset + 5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
    +    buf[offset + 6] = tid & 0xFF  # LSB of the Transition ID
    +    buf[offset + 7] = 0x00  # Extended address
    +    buf[offset + 8] = (address >> 24) & 0xFF  # address (MSB)
    +    buf[offset + 9] = (address >> 16) & 0xFF  # address
    +    buf[offset + 10] = (address >> 8) & 0xFF  # address
    +    buf[offset + 11] = address & 0xFF  # address (LSB)
    +    buf[offset + 12] = (length >> 16) & 0xFF  # data length (MSB)
    +    buf[offset + 13] = (length >> 8) & 0xFF  # data length
    +    buf[offset + 14] = length & 0xFF  # data length (LSB)
    +    buf[offset + 15] = rmap_crc_check(buf, 0, 15) & 0xFF  # header CRC
    +
    +    offset += 16
    +
    +    for idx, value in enumerate(data):
    +        buf[offset + idx] = value
    +
    +    buf[offset + length] = rmap_crc_check(buf, offset, length) & 0xFF  # data CRC
    +
    +    return bytes(buf)
    +
    +
    +def create_rmap_write_request_reply_packet(instruction_field: int, tid: int, status: int) -> bytes:
    +    buf = bytearray(8)
    +
    +    buf[0] = 0x50  # Initiator address N-DPU or F-DPU
    +    buf[1] = 0x01  # RMAP Protocol ID
    +    buf[2] = instruction_field & 0x3F  # Clear the command bit as this is a reply
    +    buf[3] = status & 0xFF  # Status field: 0 on success
    +    buf[4] = 0x51  # Target address is always the N-FEE or F-FEE
    +    buf[5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
    +    buf[6] = tid & 0xFF  # LSB of the Transition ID
    +    buf[7] = rmap_crc_check(buf, 0, 7) & 0xFF  # Header CRC
    +
    +    return bytes(buf)
    +
    +
    +@static_vars(warning_count=0, stride=100)
    +def check_address_and_data_length(address: int, length: int, strict: bool = True) -> None:
    +    """
    +    Checks the address and length in the range of memory areas used by the FEE.
    +
    +    The ranges are taken from the PLATO-DLR-PL-ICD-0010 N-FEE to N-DPU IRD.
    +
    +    Args:
    +        address (int): the memory address of the FEE Register
    +        length (int): the number of bytes requested
    +        strict (bool): strictly apply the rules
    +
    +    Raises:
    +        RMAPError: when address + length fall outside any specified area.
    +    """
    +
    +    if not strict:
    +        # All these restrictions have been relaxed on the N-FEE.
    +        # We are returning here immediately instead of removing or commenting out the code.
    +        # The reason is that we can then bring back restriction easier and gradually.
    +
    +        # A warning is sent out
    +
    +        if not check_address_and_data_length.warning_count % check_address_and_data_length.stride:
    +            check_address_and_data_length.warning_count = 1
    +            _LOGGER.warning(
    +                "Address and data length checks have been disabled, because the N-FEE "
    +                "does not enforce restrictions in the critical memory area.")
    +
    +        check_address_and_data_length.warning_count += 1
    +
    +        return
    +
    +    if length % 4:
    +        raise RMAPError(
    +            "The requested data length shall be a multiple of 4 bytes.", address, length
    +        )
    +
    +    if address % 4:
    +        raise RMAPError("The address shall be a multiple of 4 bytes.", address, length)
    +
    +    # Note that when checking the given data length, at the defined area end,
    +    # we can still read 4 bytes.
    +
    +    if CRITICAL_AREA_START <= address <= CRITICAL_AREA_END:
    +        if length != 4:
    +            raise RMAPError(
    +                "Read requests to the critical area have a fixed data length of 4 bytes.",
    +                address, length
    +            )
    +    elif GENERAL_AREA_START <= address <= GENERAL_AREA_END:
    +        if length > 256:
    +            raise RMAPError(
    +                "Read requests to the general area have a maximum data length of 256 bytes.",
    +                address, length
    +            )
    +        if address + length > GENERAL_AREA_END + 4:
    +            raise RMAPError(
    +                "The requested data length for the general area is too large. "
    +                "The address + length exceeds the general area boundaries.",
    +                address, length
    +            )
    +
    +    elif HK_AREA_START <= address <= HK_AREA_END:
    +        if length > 256:
    +            raise RMAPError(
    +                "Read requests to the housekeeping area have a maximum data length of 256 bytes.",
    +                address, length
    +            )
    +        if address + length > HK_AREA_END + 4:
    +            raise RMAPError(
    +                "The requested data length for the housekeeping area is too large. "
    +                "The address + length exceeds the housekeeping area boundaries.",
    +                address, length
    +            )
    +
    +    elif WINDOWING_AREA_START <= address <= WINDOWING_AREA_END:
    +        if length > 4096:
    +            raise RMAPError(
    +                "Read requests to the windowing area have a maximum data length of 4096 bytes.",
    +                address, length
    +            )
    +        if address + length > WINDOWING_AREA_END + 4:
    +            raise RMAPError(
    +                "The requested data length for the windowing area is too large. "
    +                "The address + length exceeds the windowing area boundaries.", address, length
    +            )
    +
    +    else:
    +        raise RMAPError("Register address for RMAP read requests is invalid.", address, length)
    +
    +
    +def rmap_crc_check(data, start, length) -> int:
    +    """Calculate the checksum for the given data."""
    +    return crc_calc(data, start, length)
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +def check_address_and_data_length(address: int, length: int, strict: bool = True) ‑> None +
    +
    +

    Checks the address and length in the range of memory areas used by the FEE.

    +

    The ranges are taken from the PLATO-DLR-PL-ICD-0010 N-FEE to N-DPU IRD.

    +

    Args

    +
    +
    address : int
    +
    the memory address of the FEE Register
    +
    length : int
    +
    the number of bytes requested
    +
    strict : bool
    +
    strictly apply the rules
    +
    +

    Raises

    +
    +
    RMAPError
    +
    when address + length fall outside any specified area.
    +
    +
    + +Expand source code + +
    @static_vars(warning_count=0, stride=100)
    +def check_address_and_data_length(address: int, length: int, strict: bool = True) -> None:
    +    """
    +    Checks the address and length in the range of memory areas used by the FEE.
    +
    +    The ranges are taken from the PLATO-DLR-PL-ICD-0010 N-FEE to N-DPU IRD.
    +
    +    Args:
    +        address (int): the memory address of the FEE Register
    +        length (int): the number of bytes requested
    +        strict (bool): strictly apply the rules
    +
    +    Raises:
    +        RMAPError: when address + length fall outside any specified area.
    +    """
    +
    +    if not strict:
    +        # All these restrictions have been relaxed on the N-FEE.
    +        # We are returning here immediately instead of removing or commenting out the code.
    +        # The reason is that we can then bring back restriction easier and gradually.
    +
    +        # A warning is sent out
    +
    +        if not check_address_and_data_length.warning_count % check_address_and_data_length.stride:
    +            check_address_and_data_length.warning_count = 1
    +            _LOGGER.warning(
    +                "Address and data length checks have been disabled, because the N-FEE "
    +                "does not enforce restrictions in the critical memory area.")
    +
    +        check_address_and_data_length.warning_count += 1
    +
    +        return
    +
    +    if length % 4:
    +        raise RMAPError(
    +            "The requested data length shall be a multiple of 4 bytes.", address, length
    +        )
    +
    +    if address % 4:
    +        raise RMAPError("The address shall be a multiple of 4 bytes.", address, length)
    +
    +    # Note that when checking the given data length, at the defined area end,
    +    # we can still read 4 bytes.
    +
    +    if CRITICAL_AREA_START <= address <= CRITICAL_AREA_END:
    +        if length != 4:
    +            raise RMAPError(
    +                "Read requests to the critical area have a fixed data length of 4 bytes.",
    +                address, length
    +            )
    +    elif GENERAL_AREA_START <= address <= GENERAL_AREA_END:
    +        if length > 256:
    +            raise RMAPError(
    +                "Read requests to the general area have a maximum data length of 256 bytes.",
    +                address, length
    +            )
    +        if address + length > GENERAL_AREA_END + 4:
    +            raise RMAPError(
    +                "The requested data length for the general area is too large. "
    +                "The address + length exceeds the general area boundaries.",
    +                address, length
    +            )
    +
    +    elif HK_AREA_START <= address <= HK_AREA_END:
    +        if length > 256:
    +            raise RMAPError(
    +                "Read requests to the housekeeping area have a maximum data length of 256 bytes.",
    +                address, length
    +            )
    +        if address + length > HK_AREA_END + 4:
    +            raise RMAPError(
    +                "The requested data length for the housekeeping area is too large. "
    +                "The address + length exceeds the housekeeping area boundaries.",
    +                address, length
    +            )
    +
    +    elif WINDOWING_AREA_START <= address <= WINDOWING_AREA_END:
    +        if length > 4096:
    +            raise RMAPError(
    +                "Read requests to the windowing area have a maximum data length of 4096 bytes.",
    +                address, length
    +            )
    +        if address + length > WINDOWING_AREA_END + 4:
    +            raise RMAPError(
    +                "The requested data length for the windowing area is too large. "
    +                "The address + length exceeds the windowing area boundaries.", address, length
    +            )
    +
    +    else:
    +        raise RMAPError("Register address for RMAP read requests is invalid.", address, length)
    +
    +
    +
    +def create_rmap_read_request_packet(address: int, length: int, tid: int, strict: bool = True) ‑> bytes +
    +
    +

    Creates an RMAP Read Request SpaceWire packet.

    +

    The read request is an RMAP command that read a number of bytes from the FEE register memory.

    +

    The function returns a ctypes character array (which is basically a bytes array) that +can be passed into the EtherSpaceLink library function esl_write_packet().

    +

    Address shall be within the 0x0000_0000 and 0x00FF_FFFC. The memory map (register) is divided +in the following areas:

    +
    0x0000_0000 - 0x0000_00FC   Critical Configuration Area (verified write)
    +0x0000_0100 - 0x0000_06FC   General Configuration Area (unverified write)
    +0x0000_0700 - 0x0000_07FC   Housekeeping area
    +0x0000_0800 - 0x007F_FFFC   Not Supported
    +0x0080_0000 - 0x00FF_FFFC   Windowing Area (unverified write)
    +0x0010_0000 - 0xFFFF_FFFC   Not Supported
    +
    +

    All read requests to the critical area shall have a fixed data length of 4 bytes. +All read requests to a general area shall have a maximum data length of 256 bytes. +All read requests to the housekeeping area shall have a maximum data length of 256 bytes. +All read requests to the windowing area shall have a maximum data length of 4096 bytes.

    +

    The transaction identifier shall be incremented for each read request. This shall be done by +the calling function!

    +

    Args

    +
    +
    address : int
    +
    the FEE register memory address
    +
    length : int
    +
    the data length
    +
    tid : int
    +
    transaction identifier
    +
    strict : bool
    +
    perform strict checking of address and length
    +
    +

    Returns

    +

    a bytes object containing the full RMAP Read Request packet.

    +
    + +Expand source code + +
    def create_rmap_read_request_packet(address: int, length: int, tid: int, strict: bool = True) -> bytes:
    +    """
    +    Creates an RMAP Read Request SpaceWire packet.
    +
    +    The read request is an RMAP command that read a number of bytes from the FEE register memory.
    +
    +    The function returns a ``ctypes`` character array (which is basically a bytes array) that
    +    can be passed into the EtherSpaceLink library function ``esl_write_packet()``.
    +
    +    Address shall be within the 0x0000_0000 and 0x00FF_FFFC. The memory map (register) is divided
    +    in the following areas:
    +
    +        0x0000_0000 - 0x0000_00FC   Critical Configuration Area (verified write)
    +        0x0000_0100 - 0x0000_06FC   General Configuration Area (unverified write)
    +        0x0000_0700 - 0x0000_07FC   Housekeeping area
    +        0x0000_0800 - 0x007F_FFFC   Not Supported
    +        0x0080_0000 - 0x00FF_FFFC   Windowing Area (unverified write)
    +        0x0010_0000 - 0xFFFF_FFFC   Not Supported
    +
    +    All read requests to the critical area shall have a fixed data length of 4 bytes.
    +    All read requests to a general area shall have a maximum data length of 256 bytes.
    +    All read requests to the housekeeping area shall have a maximum data length of 256 bytes.
    +    All read requests to the windowing area shall have a maximum data length of 4096 bytes.
    +
    +    The transaction identifier shall be incremented for each read request. This shall be done by
    +    the calling function!
    +
    +    Args:
    +        address (int): the FEE register memory address
    +        length (int): the data length
    +        tid (int): transaction identifier
    +        strict (bool): perform strict checking of address and length
    +
    +    Returns:
    +        a bytes object containing the full RMAP Read Request packet.
    +    """
    +    from egse.spw import update_transaction_identifier
    +
    +    check_address_and_data_length(address, length, strict=strict)
    +
    +    tid = update_transaction_identifier(tid)
    +
    +    buf = bytearray(16)
    +
    +    # NOTE: The first bytes would each carry the target SpW address or a destination port,
    +    #       but this is not used for point-to-point connections, so we're safe.
    +
    +    buf[0] = 0x51  # Target N-FEE or F-FEE
    +    buf[1] = 0x01  # RMAP Protocol ID
    +    buf[2] = 0x4C  # Instruction: 0b1001100, RMAP Request, Read, Incrementing address, reply address
    +    buf[3] = 0xD1  # Destination Key
    +    buf[4] = 0x50  # Initiator is always the DPU
    +    buf[5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
    +    buf[6] = tid & 0xFF  # LSB of the Transition ID
    +    buf[7] = 0x00  # Extended address is not used
    +    buf[8] = (address >> 24) & 0xFF  # address (MSB)
    +    buf[9] = (address >> 16) & 0xFF  # address
    +    buf[10] = (address >> 8) & 0xFF  # address
    +    buf[11] = address & 0xFF  # address (LSB)
    +    buf[12] = (length >> 16) & 0xFF  # data length (MSB)
    +    buf[13] = (length >> 8) & 0xFF  # data length
    +    buf[14] = length & 0xFF  # data length (LSB)
    +    buf[15] = rmap_crc_check(buf, 0, 15) & 0xFF
    +
    +    return bytes(buf)
    +
    +
    +
    +def create_rmap_read_request_reply_packet(instruction_field: int, tid: int, status: int, buffer: bytes, buffer_length: int) ‑> bytes +
    +
    +

    Creates an RMAP Reply to a RMAP Read Request packet.

    +

    The function returns a ctypes character array (which is basically a bytes array) that +can be passed into the EtherSpaceLink library function esl_write_packet().

    +

    Args

    +
    +
    instruction_field : int
    +
    the instruction field of the RMAP read request packet
    +
    tid : int
    +
    the transaction identifier of the read request packet
    +
    status : int
    +
    shall be 0 if the read request was successful, contain an error code otherwise.
    +
    buffer : bytes
    +
    the data that was read as indicated by the read request
    +
    buffer_length : int
    +
    the data length
    +
    +

    Returns

    +
    +
    packet
    +
    a bytes object containing the full RMAP Reply packet.
    +
    +
    + +Expand source code + +
    def create_rmap_read_request_reply_packet(
    +        instruction_field: int, tid: int, status: int, buffer: bytes, buffer_length: int) -> bytes:
    +    """
    +    Creates an RMAP Reply to a RMAP Read Request packet.
    +
    +    The function returns a ``ctypes`` character array (which is basically a bytes array) that
    +    can be passed into the EtherSpaceLink library function ``esl_write_packet()``.
    +
    +    Args:
    +        instruction_field (int): the instruction field of the RMAP read request packet
    +        tid (int): the transaction identifier of the read request packet
    +        status (int): shall be 0 if the read request was successful, contain an error code otherwise.
    +        buffer (bytes): the data that was read as indicated by the read request
    +        buffer_length (int): the data length
    +
    +    Returns:
    +        packet: a bytes object containing the full RMAP Reply packet.
    +    """
    +
    +    buf = bytearray(12 + buffer_length + 1)
    +
    +    buf[0] = 0x50  # Initiator address N-DPU or F-DPU
    +    buf[1] = 0x01  # RMAP Protocol ID
    +    buf[2] = instruction_field & 0x3F  # Clear the command bit as this is a reply
    +    buf[3] = status & 0xFF  # Status field: 0 on success
    +    buf[4] = 0x51  # Target address is always the N-FEE or F-FEE
    +    buf[5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
    +    buf[6] = tid & 0xFF  # LSB of the Transition ID
    +    buf[7] = 0x00  # Reserved
    +    buf[8] = (buffer_length >> 16) & 0xFF  # data length (MSB)
    +    buf[9] = (buffer_length >> 8) & 0xFF  # data length
    +    buf[10] = buffer_length & 0xFF  # data length (LSB)
    +    buf[11] = rmap_crc_check(buf, 0, 11) & 0xFF  # Header CRC
    +
    +    # Note that we assume here that len(buffer) == buffer_length.
    +
    +    if len(buffer) != buffer_length:
    +        _LOGGER.warning(
    +            f"While creating an RMAP read reply packet, the length of the buffer ({len(buffer)}) "
    +            f"not equals the buffer_length ({buffer_length})"
    +        )
    +
    +    for idx, value in enumerate(buffer):
    +        buf[12 + idx] = value
    +
    +    buf[12 + buffer_length] = rmap_crc_check(buffer, 0, buffer_length) & 0xFF  # data CRC
    +
    +    return bytes(buf)
    +
    +
    +
    +def create_rmap_unverified_write_packet(address: int, data: bytes, length: int, tid: int) ‑> bytes +
    +
    +

    Create an RMAP packet for a unverified write request on the FEE.

    +

    Args

    +
    +
    address
    +
    the start memory address on the FEE register map
    +
    data
    +
    the data to be written in the register map at address
    +
    length
    +
    the length of the data
    +
    tid : int
    +
    transaction identifier
    +
    +

    Returns

    +
    +
    packet
    +
    a bytes object containing the SpaceWire packet.
    +
    +
    + +Expand source code + +
    def create_rmap_unverified_write_packet(address: int, data: bytes, length: int, tid: int) -> bytes:
    +    """
    +    Create an RMAP packet for a unverified write request on the FEE.
    +
    +    Args:
    +        address: the start memory address on the FEE register map
    +        data: the data to be written in the register map at address
    +        length: the length of the data
    +        tid (int): transaction identifier
    +
    +    Returns:
    +        packet: a bytes object containing the SpaceWire packet.
    +    """
    +    from egse.spw import update_transaction_identifier
    +
    +    # We can only handle data for which the length >= the given length argument.
    +
    +    if len(data) < length:
    +        raise ValueError(
    +            f"The length of the data argument ({len(data)}) is smaller than "
    +            f"the given length argument ({length})."
    +        )
    +
    +    if len(data) > length:
    +        _LOGGER.warning(
    +            f"The length of the data argument ({len(data)}) is larger than "
    +            f"the given length argument ({length}). The data will be truncated "
    +            f"when copied into the packet."
    +        )
    +
    +    if address <= CRITICAL_AREA_END:
    +        raise ValueError(
    +            f"The given address (0x{address:08X}) is in the range for critical configuration is "
    +            f"[0x00 - 0xFC]. Use the verified write function for this."
    +        )
    +
    +    tid = update_transaction_identifier(tid)
    +
    +    # Buffer length is fixed at 24 bytes since the data length
    +    # is fixed at 4 bytes (32 bit addressing)
    +
    +    buf = bytearray(16 + length + 1)
    +    offset = 0
    +
    +    buf[offset + 0] = 0x51  # Logical Address
    +    buf[offset + 1] = 0x01  # Protocol ID
    +    buf[offset + 2] = 0x6C  # Instruction
    +    buf[offset + 3] = 0xD1  # Key
    +    buf[offset + 4] = 0x50  # Initiator Address
    +    buf[offset + 5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
    +    buf[offset + 6] = tid & 0xFF  # LSB of the Transition ID
    +    buf[offset + 7] = 0x00  # Extended address
    +    buf[offset + 8] = (address >> 24) & 0xFF  # address (MSB)
    +    buf[offset + 9] = (address >> 16) & 0xFF  # address
    +    buf[offset + 10] = (address >> 8) & 0xFF  # address
    +    buf[offset + 11] = address & 0xFF  # address (LSB)
    +    buf[offset + 12] = (length >> 16) & 0xFF  # data length (MSB)
    +    buf[offset + 13] = (length >> 8) & 0xFF  # data length
    +    buf[offset + 14] = length & 0xFF  # data length (LSB)
    +    buf[offset + 15] = rmap_crc_check(buf, 0, 15) & 0xFF  # header CRC
    +
    +    offset += 16
    +
    +    for idx, value in enumerate(data):
    +        buf[offset + idx] = value
    +
    +    buf[offset + length] = rmap_crc_check(buf, offset, length) & 0xFF  # data CRC
    +
    +    return bytes(buf)
    +
    +
    +
    +def create_rmap_verified_write_packet(address: int, data: bytes, tid: int) ‑> bytes +
    +
    +

    Create an RMAP packet for a verified write request on the FEE. The length of the data is +by convention always 4 bytes and therefore not passed as an argument.

    +

    Args

    +
    +
    address
    +
    the start memory address on the FEE register map
    +
    data
    +
    the data to be written in the register map at address [4 bytes]
    +
    tid : int
    +
    transaction identifier
    +
    +

    Returns

    +
    +
    packet
    +
    a bytes object containing the SpaceWire packet.
    +
    +
    + +Expand source code + +
    def create_rmap_verified_write_packet(address: int, data: bytes, tid: int) -> bytes:
    +    """
    +    Create an RMAP packet for a verified write request on the FEE. The length of the data is
    +    by convention always 4 bytes and therefore not passed as an argument.
    +
    +    Args:
    +        address: the start memory address on the FEE register map
    +        data: the data to be written in the register map at address [4 bytes]
    +        tid (int): transaction identifier
    +
    +    Returns:
    +        packet: a bytes object containing the SpaceWire packet.
    +    """
    +    from egse.spw import update_transaction_identifier
    +
    +    if len(data) < 4:
    +        raise ValueError(
    +            f"The data argument should be at least 4 bytes, but it is only {len(data)} bytes: {data=}.")
    +
    +    if address > CRITICAL_AREA_END:
    +        raise ValueError("The address range for critical configuration is [0x00 - 0xFC].")
    +
    +    tid = update_transaction_identifier(tid)
    +
    +    # Buffer length is fixed at 24 bytes since the data length is fixed
    +    # at 4 bytes (32 bit addressing)
    +
    +    buf = bytearray(21)
    +
    +    # The values below are taken from the PLATO N-FEE to N-DPU
    +    # Interface Requirements Document [PLATO-DLR-PL-ICD-0010]
    +
    +    buf[0] = 0x51  # Logical Address
    +    buf[1] = 0x01  # Protocol ID
    +    buf[2] = 0x7C  # Instruction
    +    buf[3] = 0xD1  # Key
    +    buf[4] = 0x50  # Initiator Address
    +    buf[5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
    +    buf[6] = tid & 0xFF  # LSB of the Transition ID
    +    buf[7] = 0x00  # Extended address
    +    buf[8] = (address >> 24) & 0xFF  # address (MSB)
    +    buf[9] = (address >> 16) & 0xFF  # address
    +    buf[10] = (address >> 8) & 0xFF  # address
    +    buf[11] = address & 0xFF  # address (LSB)
    +    buf[12] = 0x00  # data length (MSB)
    +    buf[13] = 0x00  # data length
    +    buf[14] = 0x04  # data length (LSB)
    +    buf[15] = rmap_crc_check(buf, 0, 15) & 0xFF  # header CRC
    +    buf[16] = data[0]
    +    buf[17] = data[1]
    +    buf[18] = data[2]
    +    buf[19] = data[3]
    +    buf[20] = rmap_crc_check(buf, 16, 4) & 0xFF  # data CRC
    +
    +    return bytes(buf)
    +
    +
    +
    +def create_rmap_write_request_reply_packet(instruction_field: int, tid: int, status: int) ‑> bytes +
    +
    +
    +
    + +Expand source code + +
    def create_rmap_write_request_reply_packet(instruction_field: int, tid: int, status: int) -> bytes:
    +    buf = bytearray(8)
    +
    +    buf[0] = 0x50  # Initiator address N-DPU or F-DPU
    +    buf[1] = 0x01  # RMAP Protocol ID
    +    buf[2] = instruction_field & 0x3F  # Clear the command bit as this is a reply
    +    buf[3] = status & 0xFF  # Status field: 0 on success
    +    buf[4] = 0x51  # Target address is always the N-FEE or F-FEE
    +    buf[5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
    +    buf[6] = tid & 0xFF  # LSB of the Transition ID
    +    buf[7] = rmap_crc_check(buf, 0, 7) & 0xFF  # Header CRC
    +
    +    return bytes(buf)
    +
    +
    +
    +def rmap_crc_check(data, start, length) ‑> int +
    +
    +

    Calculate the checksum for the given data.

    +
    + +Expand source code + +
    def rmap_crc_check(data, start, length) -> int:
    +    """Calculate the checksum for the given data."""
    +    return crc_calc(data, start, length)
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class RMAPError +(*args, **kwargs) +
    +
    +

    An RMAP specific Error.

    +
    + +Expand source code + +
    class RMAPError(Error):
    +    """An RMAP specific Error."""
    +    pass
    +
    +

    Ancestors

    + +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/api/egse/search.html b/docs/api/egse/search.html index a993988..ee3ca76 100644 --- a/docs/api/egse/search.html +++ b/docs/api/egse/search.html @@ -432,7 +432,7 @@

    Classes

    Ancestors

      -
    • typing.Protocol
    • +
    • typing_extensions.Protocol
    • typing.Generic
    diff --git a/docs/api/egse/serialdevice.html b/docs/api/egse/serialdevice.html index 635d2e7..a7f9a52 100644 --- a/docs/api/egse/serialdevice.html +++ b/docs/api/egse/serialdevice.html @@ -426,7 +426,6 @@

    Subclasses

  • Smd3Controller
  • Agilent34970DeviceInterface
  • Igm402Controller
  • -
  • VacscanController
  • Acp40Controller
  • Tc400Controller
  • diff --git a/docs/api/egse/services.html b/docs/api/egse/services.html index 5b53b3a..bf3ec7d 100644 --- a/docs/api/egse/services.html +++ b/docs/api/egse/services.html @@ -46,13 +46,14 @@

    Module egse.services

    from egse.command import ClientServerCommand from egse.control import ControlServer +from egse.decorators import dynamic_interface from egse.protocol import CommandProtocol from egse.proxy import Proxy from egse.settings import Settings from egse.zmq_ser import bind_address from egse.zmq_ser import connect_address -MODULE_LOGGER = logging.getLogger(__name__) +LOGGER = logging.getLogger(__name__) SERVICE_SETTINGS = Settings.load(filename="services.yaml") @@ -74,7 +75,7 @@

    Module egse.services

    def get_status(self): return super().get_status() - def handle_monitoring_frequency(self, freq: float): + def handle_set_monitoring_frequency(self, freq: float): """ Sets the monitoring frequency (Hz) to the given freq value. This is only approximate since the frequency is converted into a delay time and the actual execution of the status function is subject to the load on the @@ -84,15 +85,15 @@

    Module egse.services

    freq: frequency of execution (Hz) Returns: - Sends back the selected delay time in seconds. + Sends back the selected delay time in milliseconds. """ - seconds = self.control_server.set_delay(1.0 / freq) + delay = self.control_server.set_delay(1.0 / freq) - MODULE_LOGGER.debug(f"Set monitoring frequency to {freq}Hz, ± every {seconds} seconds.") + LOGGER.debug(f"Set monitoring frequency to {freq}Hz, ± every {delay:.0f}ms.") - self.send(seconds) + self.send(delay) - def handle_hk_frequency(self, freq: float): + def handle_set_hk_frequency(self, freq: float): """ Sets the housekeeping frequency (Hz) to the given freq value. This is only approximate since the frequency is converted into a delay time and the actual execution of the `housekeeping` function is subject to the load on @@ -102,15 +103,15 @@

    Module egse.services

    freq: frequency of execution (Hz) Returns: - Sends back the selected delay time in seconds. + Sends back the selected delay time in milliseconds. """ - seconds = self.control_server.set_hk_delay(1.0 / freq) + delay = self.control_server.set_hk_delay(1.0 / freq) - MODULE_LOGGER.debug(f"Set housekeeping frequency to {freq}Hz, ± every {seconds} seconds.") + LOGGER.debug(f"Set housekeeping frequency to {freq}Hz, ± every {delay:.0f}ms.") - self.send(seconds) + self.send(delay) - def handle_logging_level(self, *args, **kwargs): + def handle_set_logging_level(self, *args, **kwargs): """ Set the logging level for the logger with the given name. @@ -129,7 +130,7 @@

    Module egse.services

    level = int(args[1]) else: name = kwargs['name'] - level = kwargs['level'] + level = int(kwargs['level']) if name == 'all': for logger in [logging.getLogger(logger_name) @@ -149,25 +150,91 @@

    Module egse.services

    self.send(msg) def handle_quit(self): - MODULE_LOGGER.info(f"Sending interrupt to {self.control_server.__class__.__name__}.") + LOGGER.info(f"Sending interrupt to {self.control_server.__class__.__name__}.") self.control_server.quit() self.send(f"Sent interrupt to {self.control_server.__class__.__name__}.") - def handle_process_status(self): - MODULE_LOGGER.debug(f"Asking for process status of {self.control_server.__class__.__name__}.") + def handle_get_process_status(self): + LOGGER.debug(f"Asking for process status of {self.control_server.__class__.__name__}.") self.send(self.get_status()) - # FIXME: This function should be renamed `handle_get_cs_module` in the same spirit as all other service commands - - def get_cs_module(self): + def handle_get_cs_module(self): """ Returns the module in which the control server has been implemented. """ - MODULE_LOGGER.debug("Asking for module of {self.control_server.__class__.__name__}.") + LOGGER.debug(f"Asking for module of {self.control_server.__class__.__name__}.") self.send(inspect.getmodule(self.control_server).__spec__.name) - -class ServiceProxy(Proxy): + def handle_get_average_execution_times(self): + LOGGER.debug(f"Asking for average execution times of {self.control_server.__class__.__name__} functions.") + self.send(self.control_server.get_average_execution_times()) + + def handle_get_storage_mnemonic(self): + LOGGER.debug(f"Asking for the storage menmonic of {self.control_server.__class__.__name__}.") + self.send(self.control_server.get_storage_mnemonic()) + + def handle_add_listener(self, listener: dict): + LOGGER.debug(f"Add listener to {self.control_server.__class__.__name__}: {listener}") + try: + self.control_server.listeners.add_listener(listener) + LOGGER.info(f"Registered listener: {listener['name']} with proxy {listener['proxy']}") + self.send(("ACK",)) + except ValueError as exc: + self.send(("NACK", exc)) # Why not send back a failure object? + + def handle_remove_listener(self, listener: dict): + LOGGER.debug(f"Remove listener from {self.control_server.__class__.__name__}: {listener}") + try: + self.control_server.listeners.remove_listener(listener) + LOGGER.info(f"Removed listener: {listener['name']}") + self.send(("ACK",)) + except ValueError as exc: + self.send(("NACK", exc)) # Why not send back a failure object? + + def handle_get_listener_names(self): + LOGGER.debug(f"Get names of registered listener from {self.control_server.__class__.__name__}") + try: + names = self.control_server.listeners.get_listener_names() + self.send((names,)) + except ValueError as exc: + self.send(("", exc)) # Why not sent back a Failure object? + +class ServiceInterface: + @dynamic_interface + def set_monitoring_frequency(self, freq: float): + ... + @dynamic_interface + def set_hk_frequency(self, freq: float): + ... + @dynamic_interface + def set_logging_level(self, name: str, level: int): + ... + @dynamic_interface + def quit_server(self): + ... + @dynamic_interface + def get_process_status(self): + ... + @dynamic_interface + def get_cs_module(self): + ... + @dynamic_interface + def get_average_execution_times(self): + ... + @dynamic_interface + def get_storage_mnemonic(self): + ... + @dynamic_interface + def add_listener(self, listener: dict): + ... + @dynamic_interface + def remove_listener(self, listener: dict): + ... + @dynamic_interface + def get_listener_names(self, listener: dict): + ... + +class ServiceProxy(Proxy, ServiceInterface): """ A ServiceProxy is a simple class that forwards service commands to a control server. """ @@ -208,7 +275,7 @@

    Module egse.services

    _port = port if _hostname is None or _port is None: - raise ValueError(f"expected ctrl-settings or hostname and port as arguments") + raise ValueError("Expected ctrl-settings or hostname and port as arguments") super().__init__(connect_address(_protocol, _hostname, _port))
    @@ -255,6 +322,212 @@

    Inherited members

    +
    +class ServiceInterface +
    +
    +
    +
    + +Expand source code + +
    class ServiceInterface:
    +    @dynamic_interface
    +    def set_monitoring_frequency(self, freq: float):
    +        ...
    +    @dynamic_interface
    +    def set_hk_frequency(self, freq: float):
    +        ...
    +    @dynamic_interface
    +    def set_logging_level(self, name: str, level: int):
    +        ...
    +    @dynamic_interface
    +    def quit_server(self):
    +        ...
    +    @dynamic_interface
    +    def get_process_status(self):
    +        ...
    +    @dynamic_interface
    +    def get_cs_module(self):
    +        ...
    +    @dynamic_interface
    +    def get_average_execution_times(self):
    +        ...
    +    @dynamic_interface
    +    def get_storage_mnemonic(self):
    +        ...
    +    @dynamic_interface
    +    def add_listener(self, listener: dict):
    +        ...
    +    @dynamic_interface
    +    def remove_listener(self, listener: dict):
    +        ...
    +    @dynamic_interface
    +    def get_listener_names(self, listener: dict):
    +        ...
    +
    +

    Subclasses

    + +

    Methods

    +
    +
    +def add_listener(self, listener: dict) +
    +
    +
    +
    + +Expand source code + +
    @dynamic_interface
    +def add_listener(self, listener: dict):
    +    ...
    +
    +
    +
    +def get_average_execution_times(self) +
    +
    +
    +
    + +Expand source code + +
    @dynamic_interface
    +def get_average_execution_times(self):
    +    ...
    +
    +
    +
    +def get_cs_module(self) +
    +
    +
    +
    + +Expand source code + +
    @dynamic_interface
    +def get_cs_module(self):
    +    ...
    +
    +
    +
    +def get_listener_names(self, listener: dict) +
    +
    +
    +
    + +Expand source code + +
    @dynamic_interface
    +def get_listener_names(self, listener: dict):
    +    ...
    +
    +
    +
    +def get_process_status(self) +
    +
    +
    +
    + +Expand source code + +
    @dynamic_interface
    +def get_process_status(self):
    +    ...
    +
    +
    +
    +def get_storage_mnemonic(self) +
    +
    +
    +
    + +Expand source code + +
    @dynamic_interface
    +def get_storage_mnemonic(self):
    +    ...
    +
    +
    +
    +def quit_server(self) +
    +
    +
    +
    + +Expand source code + +
    @dynamic_interface
    +def quit_server(self):
    +    ...
    +
    +
    +
    +def remove_listener(self, listener: dict) +
    +
    +
    +
    + +Expand source code + +
    @dynamic_interface
    +def remove_listener(self, listener: dict):
    +    ...
    +
    +
    +
    +def set_hk_frequency(self, freq: float) +
    +
    +
    +
    + +Expand source code + +
    @dynamic_interface
    +def set_hk_frequency(self, freq: float):
    +    ...
    +
    +
    +
    +def set_logging_level(self, name: str, level: int) +
    +
    +
    +
    + +Expand source code + +
    @dynamic_interface
    +def set_logging_level(self, name: str, level: int):
    +    ...
    +
    +
    +
    +def set_monitoring_frequency(self, freq: float) +
    +
    +
    +
    + +Expand source code + +
    @dynamic_interface
    +def set_monitoring_frequency(self, freq: float):
    +    ...
    +
    +
    +
    +
    class ServiceProtocol (control_server: ControlServer) @@ -285,7 +558,7 @@

    Inherited members

    def get_status(self): return super().get_status() - def handle_monitoring_frequency(self, freq: float): + def handle_set_monitoring_frequency(self, freq: float): """ Sets the monitoring frequency (Hz) to the given freq value. This is only approximate since the frequency is converted into a delay time and the actual execution of the status function is subject to the load on the @@ -295,15 +568,15 @@

    Inherited members

    freq: frequency of execution (Hz) Returns: - Sends back the selected delay time in seconds. + Sends back the selected delay time in milliseconds. """ - seconds = self.control_server.set_delay(1.0 / freq) + delay = self.control_server.set_delay(1.0 / freq) - MODULE_LOGGER.debug(f"Set monitoring frequency to {freq}Hz, ± every {seconds} seconds.") + LOGGER.debug(f"Set monitoring frequency to {freq}Hz, ± every {delay:.0f}ms.") - self.send(seconds) + self.send(delay) - def handle_hk_frequency(self, freq: float): + def handle_set_hk_frequency(self, freq: float): """ Sets the housekeeping frequency (Hz) to the given freq value. This is only approximate since the frequency is converted into a delay time and the actual execution of the `housekeeping` function is subject to the load on @@ -313,15 +586,15 @@

    Inherited members

    freq: frequency of execution (Hz) Returns: - Sends back the selected delay time in seconds. + Sends back the selected delay time in milliseconds. """ - seconds = self.control_server.set_hk_delay(1.0 / freq) + delay = self.control_server.set_hk_delay(1.0 / freq) - MODULE_LOGGER.debug(f"Set housekeeping frequency to {freq}Hz, ± every {seconds} seconds.") + LOGGER.debug(f"Set housekeeping frequency to {freq}Hz, ± every {delay:.0f}ms.") - self.send(seconds) + self.send(delay) - def handle_logging_level(self, *args, **kwargs): + def handle_set_logging_level(self, *args, **kwargs): """ Set the logging level for the logger with the given name. @@ -340,7 +613,7 @@

    Inherited members

    level = int(args[1]) else: name = kwargs['name'] - level = kwargs['level'] + level = int(kwargs['level']) if name == 'all': for logger in [logging.getLogger(logger_name) @@ -360,22 +633,54 @@

    Inherited members

    self.send(msg) def handle_quit(self): - MODULE_LOGGER.info(f"Sending interrupt to {self.control_server.__class__.__name__}.") + LOGGER.info(f"Sending interrupt to {self.control_server.__class__.__name__}.") self.control_server.quit() self.send(f"Sent interrupt to {self.control_server.__class__.__name__}.") - def handle_process_status(self): - MODULE_LOGGER.debug(f"Asking for process status of {self.control_server.__class__.__name__}.") + def handle_get_process_status(self): + LOGGER.debug(f"Asking for process status of {self.control_server.__class__.__name__}.") self.send(self.get_status()) - # FIXME: This function should be renamed `handle_get_cs_module` in the same spirit as all other service commands - - def get_cs_module(self): + def handle_get_cs_module(self): """ Returns the module in which the control server has been implemented. """ - MODULE_LOGGER.debug("Asking for module of {self.control_server.__class__.__name__}.") - self.send(inspect.getmodule(self.control_server).__spec__.name)
    + LOGGER.debug(f"Asking for module of {self.control_server.__class__.__name__}.") + self.send(inspect.getmodule(self.control_server).__spec__.name) + + def handle_get_average_execution_times(self): + LOGGER.debug(f"Asking for average execution times of {self.control_server.__class__.__name__} functions.") + self.send(self.control_server.get_average_execution_times()) + + def handle_get_storage_mnemonic(self): + LOGGER.debug(f"Asking for the storage menmonic of {self.control_server.__class__.__name__}.") + self.send(self.control_server.get_storage_mnemonic()) + + def handle_add_listener(self, listener: dict): + LOGGER.debug(f"Add listener to {self.control_server.__class__.__name__}: {listener}") + try: + self.control_server.listeners.add_listener(listener) + LOGGER.info(f"Registered listener: {listener['name']} with proxy {listener['proxy']}") + self.send(("ACK",)) + except ValueError as exc: + self.send(("NACK", exc)) # Why not send back a failure object? + + def handle_remove_listener(self, listener: dict): + LOGGER.debug(f"Remove listener from {self.control_server.__class__.__name__}: {listener}") + try: + self.control_server.listeners.remove_listener(listener) + LOGGER.info(f"Removed listener: {listener['name']}") + self.send(("ACK",)) + except ValueError as exc: + self.send(("NACK", exc)) # Why not send back a failure object? + + def handle_get_listener_names(self): + LOGGER.debug(f"Get names of registered listener from {self.control_server.__class__.__name__}") + try: + names = self.control_server.listeners.get_listener_names() + self.send((names,)) + except ValueError as exc: + self.send(("", exc)) # Why not sent back a Failure object?

    Ancestors

      @@ -384,8 +689,41 @@

      Ancestors

    Methods

    -
    -def get_cs_module(self) +
    +def handle_add_listener(self, listener: dict) +
    +
    +
    +
    + +Expand source code + +
    def handle_add_listener(self, listener: dict):
    +    LOGGER.debug(f"Add listener to {self.control_server.__class__.__name__}: {listener}")
    +    try:
    +        self.control_server.listeners.add_listener(listener)
    +        LOGGER.info(f"Registered listener: {listener['name']} with proxy {listener['proxy']}")
    +        self.send(("ACK",))
    +    except ValueError as exc:
    +        self.send(("NACK", exc))  # Why not send back a failure object?
    +
    +
    +
    +def handle_get_average_execution_times(self) +
    +
    +
    +
    + +Expand source code + +
    def handle_get_average_execution_times(self):
    +    LOGGER.debug(f"Asking for average execution times of {self.control_server.__class__.__name__} functions.")
    +    self.send(self.control_server.get_average_execution_times())
    +
    +
    +
    +def handle_get_cs_module(self)

    Returns the module in which the control server has been implemented.

    @@ -393,16 +731,96 @@

    Methods

    Expand source code -
    def get_cs_module(self):
    +
    def handle_get_cs_module(self):
         """
         Returns the module in which the control server has been implemented.
         """
    -    MODULE_LOGGER.debug("Asking for module of {self.control_server.__class__.__name__}.")
    +    LOGGER.debug(f"Asking for module of {self.control_server.__class__.__name__}.")
         self.send(inspect.getmodule(self.control_server).__spec__.name)
    -
    -def handle_hk_frequency(self, freq: float) +
    +def handle_get_listener_names(self) +
    +
    +
    +
    + +Expand source code + +
    def handle_get_listener_names(self):
    +    LOGGER.debug(f"Get names of registered listener from {self.control_server.__class__.__name__}")
    +    try:
    +        names = self.control_server.listeners.get_listener_names()
    +        self.send((names,))
    +    except ValueError as exc:
    +        self.send(("", exc))  # Why not sent back a Failure object?
    +
    +
    +
    +def handle_get_process_status(self) +
    +
    +
    +
    + +Expand source code + +
    def handle_get_process_status(self):
    +    LOGGER.debug(f"Asking for process status of {self.control_server.__class__.__name__}.")
    +    self.send(self.get_status())
    +
    +
    +
    +def handle_get_storage_mnemonic(self) +
    +
    +
    +
    + +Expand source code + +
    def handle_get_storage_mnemonic(self):
    +    LOGGER.debug(f"Asking for the storage menmonic of {self.control_server.__class__.__name__}.")
    +    self.send(self.control_server.get_storage_mnemonic())
    +
    +
    +
    +def handle_quit(self) +
    +
    +
    +
    + +Expand source code + +
    def handle_quit(self):
    +    LOGGER.info(f"Sending interrupt to {self.control_server.__class__.__name__}.")
    +    self.control_server.quit()
    +    self.send(f"Sent interrupt to {self.control_server.__class__.__name__}.")
    +
    +
    +
    +def handle_remove_listener(self, listener: dict) +
    +
    +
    +
    + +Expand source code + +
    def handle_remove_listener(self, listener: dict):
    +    LOGGER.debug(f"Remove listener from {self.control_server.__class__.__name__}: {listener}")
    +    try:
    +        self.control_server.listeners.remove_listener(listener)
    +        LOGGER.info(f"Removed listener: {listener['name']}")
    +        self.send(("ACK",))
    +    except ValueError as exc:
    +        self.send(("NACK", exc))  # Why not send back a failure object?
    +
    +
    +
    +def handle_set_hk_frequency(self, freq: float)

    Sets the housekeeping frequency (Hz) to the given freq value. This is only approximate since the frequency is @@ -414,12 +832,12 @@

    Args

    frequency of execution (Hz)

    Returns

    -

    Sends back the selected delay time in seconds.

    +

    Sends back the selected delay time in milliseconds.

    Expand source code -
    def handle_hk_frequency(self, freq: float):
    +
    def handle_set_hk_frequency(self, freq: float):
         """
         Sets the housekeeping frequency (Hz) to the given freq value. This is only approximate since the frequency is
         converted into a delay time and the actual execution of the `housekeeping` function is subject to the load on
    @@ -429,17 +847,17 @@ 

    Returns

    freq: frequency of execution (Hz) Returns: - Sends back the selected delay time in seconds. + Sends back the selected delay time in milliseconds. """ - seconds = self.control_server.set_hk_delay(1.0 / freq) + delay = self.control_server.set_hk_delay(1.0 / freq) - MODULE_LOGGER.debug(f"Set housekeeping frequency to {freq}Hz, ± every {seconds} seconds.") + LOGGER.debug(f"Set housekeeping frequency to {freq}Hz, ± every {delay:.0f}ms.") - self.send(seconds)
    + self.send(delay)
    -
    -def handle_logging_level(self, *args, **kwargs) +
    +def handle_set_logging_level(self, *args, **kwargs)

    Set the logging level for the logger with the given name.

    @@ -458,7 +876,7 @@

    Returns

    Expand source code -
    def handle_logging_level(self, *args, **kwargs):
    +
    def handle_set_logging_level(self, *args, **kwargs):
         """
         Set the logging level for the logger with the given name.
     
    @@ -477,7 +895,7 @@ 

    Returns

    level = int(args[1]) else: name = kwargs['name'] - level = kwargs['level'] + level = int(kwargs['level']) if name == 'all': for logger in [logging.getLogger(logger_name) @@ -497,8 +915,8 @@

    Returns

    self.send(msg)
    -
    -def handle_monitoring_frequency(self, freq: float) +
    +def handle_set_monitoring_frequency(self, freq: float)

    Sets the monitoring frequency (Hz) to the given freq value. This is only approximate since the frequency is @@ -510,12 +928,12 @@

    Args

    frequency of execution (Hz)

    Returns

    -

    Sends back the selected delay time in seconds.

    +

    Sends back the selected delay time in milliseconds.

    Expand source code -
    def handle_monitoring_frequency(self, freq: float):
    +
    def handle_set_monitoring_frequency(self, freq: float):
         """
         Sets the monitoring frequency (Hz) to the given freq value. This is only approximate since the frequency is
         converted into a delay time and the actual execution of the status function is subject to the load on the
    @@ -525,42 +943,13 @@ 

    Returns

    freq: frequency of execution (Hz) Returns: - Sends back the selected delay time in seconds. + Sends back the selected delay time in milliseconds. """ - seconds = self.control_server.set_delay(1.0 / freq) + delay = self.control_server.set_delay(1.0 / freq) - MODULE_LOGGER.debug(f"Set monitoring frequency to {freq}Hz, ± every {seconds} seconds.") + LOGGER.debug(f"Set monitoring frequency to {freq}Hz, ± every {delay:.0f}ms.") - self.send(seconds)
    -
    - -
    -def handle_process_status(self) -
    -
    -
    -
    - -Expand source code - -
    def handle_process_status(self):
    -    MODULE_LOGGER.debug(f"Asking for process status of {self.control_server.__class__.__name__}.")
    -    self.send(self.get_status())
    -
    -
    -
    -def handle_quit(self) -
    -
    -
    -
    - -Expand source code - -
    def handle_quit(self):
    -    MODULE_LOGGER.info(f"Sending interrupt to {self.control_server.__class__.__name__}.")
    -    self.control_server.quit()
    -    self.send(f"Sent interrupt to {self.control_server.__class__.__name__}.")
    + self.send(delay)
    @@ -611,7 +1000,7 @@

    Args

    Expand source code -
    class ServiceProxy(Proxy):
    +
    class ServiceProxy(Proxy, ServiceInterface):
         """
         A ServiceProxy is a simple class that forwards service commands to a control server.
         """
    @@ -652,7 +1041,7 @@ 

    Args

    _port = port if _hostname is None or _port is None: - raise ValueError(f"expected ctrl-settings or hostname and port as arguments") + raise ValueError("Expected ctrl-settings or hostname and port as arguments") super().__init__(connect_address(_protocol, _hostname, _port))
    @@ -661,6 +1050,7 @@

    Ancestors

  • Proxy
  • BaseProxy
  • ControlServerConnectionInterface
  • +
  • ServiceInterface
  • Inherited members

      @@ -705,14 +1095,35 @@

      Index

      ServiceCommand

    • +

      ServiceInterface

      + +
    • +
    • ServiceProtocol

    • diff --git a/docs/api/egse/settings.html b/docs/api/egse/settings.html index 969f7f8..9249cff 100644 --- a/docs/api/egse/settings.html +++ b/docs/api/egse/settings.html @@ -219,7 +219,7 @@

      Module egse.settings

      """ if force or filename not in cls.__memoized_yaml: - logger.debug(f"Parsing YAML configuration file {filename}.") + # logger.debug(f"Parsing YAML configuration file {filename}.") with open(filename, "r") as stream: try: @@ -570,7 +570,7 @@

      Classes

      """ if force or filename not in cls.__memoized_yaml: - logger.debug(f"Parsing YAML configuration file {filename}.") + # logger.debug(f"Parsing YAML configuration file {filename}.") with open(filename, "r") as stream: try: @@ -1014,7 +1014,7 @@

      Returns

      """ if force or filename not in cls.__memoized_yaml: - logger.debug(f"Parsing YAML configuration file {filename}.") + # logger.debug(f"Parsing YAML configuration file {filename}.") with open(filename, "r") as stream: try: diff --git a/docs/api/egse/setup.html b/docs/api/egse/setup.html index 272ba42..7706b6d 100644 --- a/docs/api/egse/setup.html +++ b/docs/api/egse/setup.html @@ -225,11 +225,8 @@

      Data Files

      from typing import Optional from typing import Union -import pandas import rich import yaml -from deepdiff import DeepDiff -from numpy import genfromtxt from rich.tree import Tree from egse.control import Failure @@ -266,6 +263,8 @@

      Data Files

      def _load_csv(resource_name: str): """Find and return the content of a CSV file.""" + from numpy import genfromtxt + parts = resource_name[5:].rsplit("/", 1) [in_dir, fn] = parts if len(parts) > 1 else [None, parts[0]] conf_location = os.environ['PLATO_CONF_DATA_LOCATION'] @@ -328,6 +327,8 @@

      Data Files

      - resource_name: Filename, preceded by "pandas//". - separator: Column separator. """ + import pandas + parts = resource_name[8:].rsplit("/", 1) [in_dir, fn] = parts if len(parts) > 1 else [None, parts[0]] conf_location = os.environ['PLATO_CONF_DATA_LOCATION'] @@ -361,19 +362,34 @@

      Data Files

      return None -def get_last_setup_id_file_path(): - return Path( - os.environ.get("PLATO_DATA_STORAGE_LOCATION") or "~").expanduser().resolve() / "last_setup_id.txt" +def get_last_setup_id_file_path(site_id: str = None) -> Path: + """ + Return the fully expanded file path of the file containing the last loaded Setup in the configuration manager. + + Args: + site_id: The SITE identifier + + """ + from egse.env import get_data_storage_location + from egse.settings import Settings + + site_id = site_id or Settings.load("SITE").ID + location = get_data_storage_location(site_id=site_id) + + return Path(location).expanduser().resolve() / "last_setup_id.txt" -def load_last_setup_id() -> int: +def load_last_setup_id(site_id: str = None) -> int: """ 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. + + Args: + site_id: The SITE identifier """ - last_setup_id_file_path = get_last_setup_id_file_path() + last_setup_id_file_path = get_last_setup_id_file_path(site_id=site_id) try: with last_setup_id_file_path.open('r') as fd: setup_id = int(fd.read().strip()) @@ -384,10 +400,17 @@

      Data Files

      return setup_id -def save_last_setup_id(setup_id: int | str): - """Makes the given Setup ID persistent, so it can be restored upon the next startup.""" +def save_last_setup_id(setup_id: int | str, site_id: str = None): + """ + Makes the given Setup ID persistent, so it can be restored upon the next startup. - last_setup_id_file_path = get_last_setup_id_file_path() + Args: + setup_id: The Setup identifier to be saved + site_id: The SITE identifier + + """ + + last_setup_id_file_path = get_last_setup_id_file_path(site_id=site_id) with last_setup_id_file_path.open('w') as fd: fd.write(f"{int(setup_id):d}") @@ -812,6 +835,7 @@

      Data Files

      def compare(setup_1: NavigableDict, setup_2: NavigableDict): from egse.device import DeviceInterface from egse.dpu import DPUSimulator + from deepdiff import DeepDiff return DeepDiff(setup_1, setup_2, exclude_types={DeviceInterface, DPUSimulator}) @@ -964,13 +988,14 @@

      Data Files

      from egse.confman import ConfigurationManagerProxy try: - with ConfigurationManagerProxy() as proxy: + with ConfigurationManagerProxy(timeout=1000) as proxy: setup = proxy.get_setup(setup_id) return setup except ConnectionError as exc: - print( - "Could not make a connection with the Configuration Manager, no Setup returned." + MODULE_LOGGER.warning( + "Could not make a connection with the Configuration Manager, no Setup retrieved, None returned." ) + return None def _check_conditions_for_get_path_of_setup_file(site_id: str) -> Path: @@ -1301,13 +1326,14 @@

      Functions

      from egse.confman import ConfigurationManagerProxy try: - with ConfigurationManagerProxy() as proxy: + with ConfigurationManagerProxy(timeout=1000) as proxy: setup = proxy.get_setup(setup_id) return setup except ConnectionError as exc: - print( - "Could not make a connection with the Configuration Manager, no Setup returned." - )
    + MODULE_LOGGER.warning( + "Could not make a connection with the Configuration Manager, no Setup retrieved, None returned." + ) + return None
    @@ -1366,24 +1392,32 @@

    Functions

    -def load_last_setup_id() ‑> int +def load_last_setup_id(site_id: str = None) ‑> int

    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.

    +If no such ID can be found, the Setup ID = 0 will be returned.

    +

    Args

    +
    +
    site_id
    +
    The SITE identifier
    +
    Expand source code -
    def load_last_setup_id() -> int:
    +
    def load_last_setup_id(site_id: str = None) -> int:
         """
         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.
    +
    +    Args:
    +        site_id: The SITE identifier
         """
     
    -    last_setup_id_file_path = get_last_setup_id_file_path()
    +    last_setup_id_file_path = get_last_setup_id_file_path(site_id=site_id)
         try:
             with last_setup_id_file_path.open('r') as fd:
                 setup_id = int(fd.read().strip())
    @@ -1485,18 +1519,32 @@ 

    Returns

    -def save_last_setup_id(setup_id: int | str) +def save_last_setup_id(setup_id: int | str, site_id: str = None)
    -

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

    +

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

    +

    Args

    +
    +
    setup_id
    +
    The Setup identifier to be saved
    +
    site_id
    +
    The SITE identifier
    +
    Expand source code -
    def save_last_setup_id(setup_id: int | str):
    -    """Makes the given Setup ID persistent, so it can be restored upon the next startup."""
    +
    def save_last_setup_id(setup_id: int | str, site_id: str = None):
    +    """
    +    Makes the given Setup ID persistent, so it can be restored upon the next startup.
    +
    +    Args:
    +        setup_id: The Setup identifier to be saved
    +        site_id: The SITE identifier
    +
    +    """
     
    -    last_setup_id_file_path = get_last_setup_id_file_path()
    +    last_setup_id_file_path = get_last_setup_id_file_path(site_id=site_id)
         with last_setup_id_file_path.open('w') as fd:
             fd.write(f"{int(setup_id):d}")
    @@ -1724,6 +1772,7 @@

    Args

    def compare(setup_1: NavigableDict, setup_2: NavigableDict): from egse.device import DeviceInterface from egse.dpu import DPUSimulator + from deepdiff import DeepDiff return DeepDiff(setup_1, setup_2, exclude_types={DeviceInterface, DPUSimulator}) @@ -1848,6 +1897,7 @@

    Static methods

    def compare(setup_1: NavigableDict, setup_2: NavigableDict): from egse.device import DeviceInterface from egse.dpu import DPUSimulator + from deepdiff import DeepDiff return DeepDiff(setup_1, setup_2, exclude_types={DeviceInterface, DPUSimulator})
    diff --git a/docs/api/egse/shutter/thorlabs/ksc101_cs.html b/docs/api/egse/shutter/thorlabs/ksc101_cs.html index f4a0bc5..862d8ec 100644 --- a/docs/api/egse/shutter/thorlabs/ksc101_cs.html +++ b/docs/api/egse/shutter/thorlabs/ksc101_cs.html @@ -334,10 +334,15 @@

    Inherited members

    • ControlServer:
    • diff --git a/docs/api/egse/shutter/thorlabs/ksc101_protocol.html b/docs/api/egse/shutter/thorlabs/ksc101_protocol.html index 893fd02..34ff4cd 100644 --- a/docs/api/egse/shutter/thorlabs/ksc101_protocol.html +++ b/docs/api/egse/shutter/thorlabs/ksc101_protocol.html @@ -30,6 +30,7 @@

      Module egse.shutter.thorlabs.ksc101_protocol

      from egse.metrics import define_metrics from egse.protocol import CommandProtocol from egse.settings import Settings +from egse.setup import load_setup from egse.shutter.thorlabs.ksc101 import ShutterKSC101Controller from egse.shutter.thorlabs.ksc101 import ShutterKSC101Interface @@ -46,6 +47,7 @@

      Module egse.shutter.thorlabs.ksc101_protocol

      def __init__(self, control_server: ControlServer): super().__init__() self.control_server = control_server + setup = load_setup() if Settings.simulation_mode(): self.shutter = ShutterKSC101Simulator() @@ -57,7 +59,7 @@

      Module egse.shutter.thorlabs.ksc101_protocol

      self.build_device_method_lookup_table(self.shutter) self.synoptics = SynopticsManagerProxy() - self.metrics = define_metrics("KSC101") + self.metrics = define_metrics("KSC101", setup=setup) def get_bind_address(self): return bind_address( @@ -126,6 +128,7 @@

      Classes

      def __init__(self, control_server: ControlServer): super().__init__() self.control_server = control_server + setup = load_setup() if Settings.simulation_mode(): self.shutter = ShutterKSC101Simulator() @@ -137,7 +140,7 @@

      Classes

      self.build_device_method_lookup_table(self.shutter) self.synoptics = SynopticsManagerProxy() - self.metrics = define_metrics("KSC101") + self.metrics = define_metrics("KSC101", setup=setup) def get_bind_address(self): return bind_address( diff --git a/docs/api/egse/shutter/thorlabs/ksc101_ui.html b/docs/api/egse/shutter/thorlabs/ksc101_ui.html index e0e48ad..cc5ae33 100644 --- a/docs/api/egse/shutter/thorlabs/ksc101_ui.html +++ b/docs/api/egse/shutter/thorlabs/ksc101_ui.html @@ -1168,7 +1168,7 @@

      Methods

      class ShutterUIView
      -

      QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      +

      QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      Expand source code diff --git a/docs/api/egse/shutter/thorlabs/sc10_cs.html b/docs/api/egse/shutter/thorlabs/sc10_cs.html index 6778e8e..565cdbf 100644 --- a/docs/api/egse/shutter/thorlabs/sc10_cs.html +++ b/docs/api/egse/shutter/thorlabs/sc10_cs.html @@ -295,10 +295,15 @@

      Inherited members

      • ControlServer:
      • diff --git a/docs/api/egse/simulator.html b/docs/api/egse/simulator.html index 74a7b74..b1d37b6 100644 --- a/docs/api/egse/simulator.html +++ b/docs/api/egse/simulator.html @@ -137,7 +137,7 @@

        Subclasses

      • EnsembleSimulator
      • Smd3Simulator
      • Igm402Simulator
      • -
      • VacscanSimulator
      • +
      • EvisionSimulator
      • Acp40Simulator
      • Tc400Simulator
      • Tpg261Simulator
      • diff --git a/docs/api/egse/socketdevice.html b/docs/api/egse/socketdevice.html index 6da3023..d35c2d5 100644 --- a/docs/api/egse/socketdevice.html +++ b/docs/api/egse/socketdevice.html @@ -479,6 +479,7 @@

        Subclasses

        Methods

        diff --git a/docs/api/egse/spw.html b/docs/api/egse/spw.html index 84bf577..b93ac33 100644 --- a/docs/api/egse/spw.html +++ b/docs/api/egse/spw.html @@ -33,12 +33,14 @@

        Module egse.spw

        import logging import os import struct +import textwrap from enum import IntEnum from typing import Tuple from typing import Union import numpy as np +import egse.rmap from egse.bits import clear_bit from egse.bits import crc_calc from egse.bits import set_bit @@ -50,54 +52,23 @@

        Module egse.spw

        try: _ = os.environ["PLATO_CAMERA_IS_EM"] + MODULE_LOGGER.warning( + textwrap.dedent("""\ + The PLATO_CAMERA_IS_EM environment variable is defined. + For the EM camera, image data and camera sensor data are interpreted as twos-complement and + converted accordingly. If the camera you are testing is not the EM camera, make sure the + PLATO_CAMERA_IS_EM environment variable is not defined when starting your control servers. + """ + ) + ) TWOS_COMPLEMENT_OFFSET = 32768 if _.capitalize() in ("1", "True", "Yes") else 0 except KeyError: TWOS_COMPLEMENT_OFFSET = 0 -# RMAP Error Codes and Constants ------------------------------------------------------------------- - -RMAP_PROTOCOL_ID = 0x01 -RMAP_TARGET_LOGICAL_ADDRESS_DEFAULT = 0xFE -RMAP_TARGET_KEY = 0xD1 - -# Error and Status Codes - -RMAP_SUCCESS = 0 -RMAP_GENERAL_ERROR = 1 -RMAP_UNUSED_PACKET_TYPE_COMMAND_CODE = 2 -RMAP_INVALID_KEY = 3 -RMAP_INVALID_DATA_CRC = 4 -RMAP_EARLY_EOP = 5 -RMAP_TOO_MUCH_DATA = 6 -RMAP_EEP = 7 -RMAP_RESERVED = 8 -RMAP_VERIFY_BUFFER_OVERRUN = 9 -RMAP_NOT_IMPLEMENTED_AUTHORISED = 10 -RMAP_RMW_DATA_LENGTH_ERROR = 11 -RMAP_INVALID_TARGET_LOGICAL_ADDRESS = 12 - -# Memory Map layout -------------------------------------------------------------------------------- - -# NOTE: These memory areas are currently equal for N-FEE and F-FEE. Don't know if this will -# change in the future. - -CRITICAL_AREA_START = 0x0000_0000 -CRITICAL_AREA_END = 0x0000_00FC -GENERAL_AREA_START = 0x0000_0100 -GENERAL_AREA_END = 0x0000_06FC -HK_AREA_START = 0x0000_0700 -HK_AREA_END = 0x0000_07FC -WINDOWING_AREA_START = 0x0080_0000 -WINDOWING_AREA_END = 0x00FF_FFFC - -class RMAPError(Error): - """An RMAP specific Error.""" - pass - -class CheckError(RMAPError): +class CheckError(Error): """ - Raised when a check fails and you want to pass a status values along with the message. + Raised when a check fails, and you want to pass a status values along with the message. """ def __init__(self, message, status): @@ -109,9 +80,11 @@

        Module egse.spw

        """ Updates the transaction identifier and returns the new value. - FIXME: document more about this identifier, where is it used, when is it checked, - when does it need to be incremented, who initializes the identifier and - who updates it, ... + This identifier shall be incremented for each RMAP Request. The RMAP Request Reply packets + shall copy the transaction ID of the RMAP Request packet in their transaction ID field. + + The transaction ID is a 16-bit field which is used to associate replies with the command + that caused the reply. Args: tid (int): The current transaction identifier @@ -123,344 +96,6 @@

        Module egse.spw

        return tid -def create_rmap_read_request_packet(address: int, length: int, tid: int, strict: bool = True) -> bytes: - """ - Creates an RMAP Read Request SpaceWire packet. - - The read request is an RMAP command that read a number of bytes from the FEE register memory. - - The function returns a ``ctypes`` character array (which is basically a bytes array) that - can be passed into the EtherSpaceLink library function ``esl_write_packet()``. - - Address shall be within the 0x0000_0000 and 0x00FF_FFFC. The memory map (register) is divided - in the following areas: - - 0x0000_0000 - 0x0000_00FC Critical Configuration Area (verified write) - 0x0000_0100 - 0x0000_06FC General Configuration Area (unverified write) - 0x0000_0700 - 0x0000_07FC Housekeeping area - 0x0000_0800 - 0x007F_FFFC Not Supported - 0x0080_0000 - 0x00FF_FFFC Windowing Area (unverified write) - 0x0010_0000 - 0xFFFF_FFFC Not Supported - - All read requests to the critical area shall have a fixed data length of 4 bytes. - All read requests to a general area shall have a maximum data length of 256 bytes. - All read requests to the housekeeping area shall have a maximum data length of 256 bytes. - All read requests to the windowing area shall have a maximum data length of 4096 bytes. - - The transaction identifier shall be incremented for each read request. This shall be done by - the calling function! - - Args: - address (int): the FEE register memory address - length (int): the data length - tid (int): transaction identifier - strict (bool): perform strict checking of address and length - - Returns: - a bytes object containing the full RMAP Read Request packet. - """ - - check_address_and_data_length(address, length, strict=strict) - - buf = bytearray(16) - - # NOTE: The first bytes would each carry the target SpW address or a destination port, - # but this is not used for point-to-point connections, so we're safe. - - buf[0] = 0x51 # Target N-FEE or F-FEE - buf[1] = 0x01 # RMAP Protocol ID - buf[2] = 0x4C # Instruction: 0b1001100, RMAP Request, Read, Incrementing address, reply address - buf[3] = 0xD1 # Destination Key - buf[4] = 0x50 # Initiator is always the DPU - buf[5] = (tid >> 8) & 0xFF # MSB of the Transition ID - buf[6] = tid & 0xFF # LSB of the Transition ID - buf[7] = 0x00 # Extended address is not used - buf[8] = (address >> 24) & 0xFF # address (MSB) - buf[9] = (address >> 16) & 0xFF # address - buf[10] = (address >> 8) & 0xFF # address - buf[11] = address & 0xFF # address (LSB) - buf[12] = (length >> 16) & 0xFF # data length (MSB) - buf[13] = (length >> 8) & 0xFF # data length - buf[14] = length & 0xFF # data length (LSB) - buf[15] = rmap_crc_check(buf, 0, 15) & 0xFF - return bytes(buf) - - -def create_rmap_read_request_reply_packet( - instruction_field: int, tid: int, status: int, buffer: bytes, buffer_length: int) -> bytes: - """ - Creates an RMAP Reply to a RMAP Read Request packet. - - The function returns a ``ctypes`` character array (which is basically a bytes array) that - can be passed into the EtherSpaceLink library function ``esl_write_packet()``. - - Args: - instruction_field (int): the instruction field of the RMAP read request packet - tid (int): the transaction identifier of the read request packet - status (int): the status field, 0 on success - buffer (bytes): the data that was read as indicated by the read request - buffer_length (int): the data length - - Returns: - packet: a bytes object containing the full RMAP Reply packet. - """ - - buf = bytearray(12 + buffer_length + 1) - - buf[0] = 0x50 # Initiator address N-DPU or F-DPU - buf[1] = 0x01 # RMAP Protocol ID - buf[2] = instruction_field & 0x3F # Clear the command bit as this is a reply - buf[3] = status & 0xFF # Status field: 0 on success - buf[4] = 0x51 # Target address is always the N-FEE or F-FEE - buf[5] = (tid >> 8) & 0xFF # MSB of the Transition ID - buf[6] = tid & 0xFF # LSB of the Transition ID - buf[7] = 0x00 # Reserved - buf[8] = (buffer_length >> 16) & 0xFF # data length (MSB) - buf[9] = (buffer_length >> 8) & 0xFF # data length - buf[10] = buffer_length & 0xFF # data length (LSB) - buf[11] = rmap_crc_check(buf, 0, 11) & 0xFF # Header CRC - - # Note that we assume here that len(buffer) == buffer_length. - - if len(buffer) != buffer_length: - MODULE_LOGGER.warning( - f"While creating an RMAP read reply packet, the length of the buffer ({len(buffer)}) " - f"not equals the buffer_length ({buffer_length})" - ) - - for idx, value in enumerate(buffer): - buf[12 + idx] = value - - buf[12 + buffer_length] = rmap_crc_check(buffer, 0, buffer_length) & 0xFF # data CRC - - return bytes(buf) - - -def create_rmap_verified_write_packet(address: int, data: bytes, tid: int) -> bytes: - """ - Create an RMAP packet for a verified write request on the FEE. The length of the data is - by convention always 4 bytes and therefore not passed as an argument. - - Args: - address: the start memory address on the FEE register map - data: the data to be written in the register map at address [4 bytes] - tid (int): transaction identifier - - Returns: - packet: a bytes object containing the SpaceWire packet. - """ - - if len(data) < 4: - raise ValueError( - f"The data argument should be at least 4 bytes, but it is only {len(data)} bytes: {data=}.") - - if address > CRITICAL_AREA_END: - raise ValueError("The address range for critical configuration is [0x00 - 0xFC].") - - tid = update_transaction_identifier(tid) - - # Buffer length is fixed at 24 bytes since the data length is fixed - # at 4 bytes (32 bit addressing) - - buf = bytearray(21) - - # The values below are taken from the PLATO N-FEE to N-DPU - # Interface Requirements Document [PLATO-DLR-PL-ICD-0010] - - buf[0] = 0x51 # Logical Address - buf[1] = 0x01 # Protocol ID - buf[2] = 0x7C # Instruction - buf[3] = 0xD1 # Key - buf[4] = 0x50 # Initiator Address - buf[5] = (tid >> 8) & 0xFF # MSB of the Transition ID - buf[6] = tid & 0xFF # LSB of the Transition ID - buf[7] = 0x00 # Extended address - buf[8] = (address >> 24) & 0xFF # address (MSB) - buf[9] = (address >> 16) & 0xFF # address - buf[10] = (address >> 8) & 0xFF # address - buf[11] = address & 0xFF # address (LSB) - buf[12] = 0x00 # data length (MSB) - buf[13] = 0x00 # data length - buf[14] = 0x04 # data length (LSB) - buf[15] = rmap_crc_check(buf, 0, 15) & 0xFF # header CRC - buf[16] = data[0] - buf[17] = data[1] - buf[18] = data[2] - buf[19] = data[3] - buf[20] = rmap_crc_check(buf, 16, 4) & 0xFF # data CRC - - return bytes(buf) - - -def create_rmap_unverified_write_packet(address: int, data: bytes, length: int, tid: int) -> bytes: - """ - Create an RMAP packet for a unverified write request on the FEE. - - Args: - address: the start memory address on the FEE register map - data: the data to be written in the register map at address - length: the length of the data - tid (int): transaction identifier - - Returns: - packet: a bytes object containing the SpaceWire packet. - """ - - # We can only handle data for which the length >= the given length argument. - - if len(data) < length: - raise ValueError( - f"The length of the data argument ({len(data)}) is smaller than " - f"the given length argument ({length})." - ) - - if len(data) > length: - MODULE_LOGGER.warning( - f"The length of the data argument ({len(data)}) is larger than " - f"the given length argument ({length}). The data will be truncated " - f"when copied into the packet." - ) - - if address <= CRITICAL_AREA_END: - raise ValueError( - f"The given address (0x{address:08X}) is in the range for critical configuration is " - f"[0x00 - 0xFC]. Use the verified write function for this." - ) - - tid = update_transaction_identifier(tid) - - # Buffer length is fixed at 24 bytes since the data length - # is fixed at 4 bytes (32 bit addressing) - - buf = bytearray(16 + length + 1) - offset = 0 - - buf[offset + 0] = 0x51 # Logical Address - buf[offset + 1] = 0x01 # Protocol ID - buf[offset + 2] = 0x6C # Instruction - buf[offset + 3] = 0xD1 # Key - buf[offset + 4] = 0x50 # Initiator Address - buf[offset + 5] = (tid >> 8) & 0xFF # MSB of the Transition ID - buf[offset + 6] = tid & 0xFF # LSB of the Transition ID - buf[offset + 7] = 0x00 # Extended address - buf[offset + 8] = (address >> 24) & 0xFF # address (MSB) - buf[offset + 9] = (address >> 16) & 0xFF # address - buf[offset + 10] = (address >> 8) & 0xFF # address - buf[offset + 11] = address & 0xFF # address (LSB) - buf[offset + 12] = (length >> 16) & 0xFF # data length (MSB) - buf[offset + 13] = (length >> 8) & 0xFF # data length - buf[offset + 14] = length & 0xFF # data length (LSB) - buf[offset + 15] = rmap_crc_check(buf, 0, 15) & 0xFF # header CRC - - offset += 16 - - for idx, value in enumerate(data): - buf[offset + idx] = value - - buf[offset + length] = rmap_crc_check(buf, offset, length) & 0xFF # data CRC - - return bytes(buf) - - -def create_rmap_write_request_reply_packet(instruction_field: int, tid: int, status: int) -> bytes: - buf = bytearray(8) - - buf[0] = 0x50 # Initiator address N-DPU or F-DPU - buf[1] = 0x01 # RMAP Protocol ID - buf[2] = instruction_field & 0x3F # Clear the command bit as this is a reply - buf[3] = status & 0xFF # Status field: 0 on success - buf[4] = 0x51 # Target address is always the N-FEE or F-FEE - buf[5] = (tid >> 8) & 0xFF # MSB of the Transition ID - buf[6] = tid & 0xFF # LSB of the Transition ID - buf[7] = rmap_crc_check(buf, 0, 7) & 0xFF # Header CRC - - return bytes(buf) - - -def check_address_and_data_length(address: int, length: int, strict: bool = True) -> None: - """ - Checks the address and length in the range of memory areas used by the FEE. - - The ranges are taken from the PLATO-DLR-PL-ICD-0010 N-FEE to N-DPU IRD. - - Args: - address (int): the memory address of the FEE Register - length (int): the number of bytes requested - strict (bool): strictly apply the rules - - Raises: - RMAPError: when address + length fall outside any specified area. - """ - - if not strict: - # All these restrictions have been relaxed on the N-FEE. - # We are returning here immediately instead of removing or commenting out the code. - # These reason is that we can then bring back restriction easier and gradually. - - MODULE_LOGGER.warning( - "Address and data length checks have been disabled, because the N-FEE " - "does not enforce restrictions in the critical memory area.") - return - - if length % 4: - raise RMAPError( - "The requested data length shall be a multiple of 4 bytes.", address, length - ) - - if address % 4: - raise RMAPError("The address shall be a multiple of 4 bytes.", address, length) - - # Note that when checking the given data length, at the defined area end, - # we can still read 4 bytes. - - if CRITICAL_AREA_START <= address <= CRITICAL_AREA_END: - if length != 4: - raise RMAPError( - "Read requests to the critical area have a fixed data length of 4 bytes.", - address, length - ) - elif GENERAL_AREA_START <= address <= GENERAL_AREA_END: - if length > 256: - raise RMAPError( - "Read requests to the general area have a maximum data length of 256 bytes.", - address, length - ) - if address + length > GENERAL_AREA_END + 4: - raise RMAPError( - "The requested data length for the general area is too large. " - "The address + length exceeds the general area boundaries.", - address, length - ) - - elif HK_AREA_START <= address <= HK_AREA_END: - if length > 256: - raise RMAPError( - "Read requests to the housekeeping area have a maximum data length of 256 bytes.", - address, length - ) - if address + length > HK_AREA_END + 4: - raise RMAPError( - "The requested data length for the housekeeping area is too large. " - "The address + length exceeds the housekeeping area boundaries.", - address, length - ) - - elif WINDOWING_AREA_START <= address <= WINDOWING_AREA_END: - if length > 4096: - raise RMAPError( - "Read requests to the windowing area have a maximum data length of 4096 bytes.", - address, length - ) - if address + length > WINDOWING_AREA_END + 4: - raise RMAPError( - "The requested data length for the windowing area is too large. " - "The address + length exceeds the windowing area boundaries.", address, length - ) - - else: - raise RMAPError("Register address for RMAP read requests is invalid.", address, length) - - class PacketType(IntEnum): """Enumeration type that defines the SpaceWire packet type.""" @@ -743,17 +378,18 @@

        Module egse.spw

        self._bytes = bytes(data) def __repr__(self): - options = np.get_printoptions() - np.set_printoptions( - formatter={"int": lambda x: f"0x{x:02x}"}, - threshold=self._threshold, - edgeitems=self._edgeitems, - linewidth=self._linewidth, + limit = 25 + data_hex = ' '.join(f"{x:02x}" for x in self._bytes[:limit]) + data_hex += '...' if len(self._bytes) > limit else '' + + msg = ( + f"{self.__class__.__name__}(0x{data_hex})" ) - msg = f"{self.__class__.__name__}({self._bytes})" - np.set_printoptions(**options) return msg + def __len__(self): + return len(self._bytes) + @property def packet_as_bytes(self): return self._bytes @@ -782,6 +418,8 @@

        Module egse.spw

        Factory method that returns a SpaceWire packet of the correct type based on the information in the header. """ + # MODULE_LOGGER.info(f"{len(data) = }") + if TimecodePacket.is_timecode_packet(data): return TimecodePacket(data) if HousekeepingPacket.is_housekeeping_packet(data): @@ -801,6 +439,11 @@

        Module egse.spw

        return SpaceWirePacket(data) +class ExtensionPacket: + def __init__(self): + pass + + class DataPacket(SpaceWirePacket): """ Base class for proprietary SpaceWire data packets that are exchanged between FEE and DPU. @@ -940,6 +583,11 @@

        Module egse.spw

        edgeitems=super()._edgeitems, linewidth=super()._linewidth, ) + limit = 50 + header_hex = ' '.join(f'{byte:02X}' for byte in self.header_as_bytes()[:limit]) + data_hex = ' '.join(f'{byte:02X}' for byte in self.data[:limit]) + data_ascii = ''.join((chr(byte) if 32 <= byte <= 126 else '.') for byte in self.data[:limit]) + msg = ( f"{self.__class__.__name__}:\n" f" Logical Address = 0x{self.logical_address:02X}\n" @@ -948,7 +596,9 @@

        Module egse.spw

        f" Type = {self._type}\n" f" Frame Counter = {self.frame_counter}\n" f" Sequence Counter = {self.sequence_counter}\n" - f" Data = \n{self.data}" + f" Header = {header_hex}\n" + f" Data HEX = {data_hex}\n" + f" Data ASC = {data_ascii}\n" ) np.set_printoptions(**options) return msg @@ -1040,7 +690,39 @@

        Module egse.spw

        return data[0] == 0x91 def __str__(self): - return f"Timecode Packet: timecode = 0x{self.timecode:x}" + return f"Timecode Packet: timecode = 0x{self.timecode:02x} ({self.timecode:2d})" + + +class RMAPRequestMixin(SpaceWirePacket): + @property + def key(self): + """Returns the key field.""" + return get_key_field(self._bytes) + + @property + def initiator_address(self): + """Returns the initiator logical address.""" + return self._bytes[4] + + @property + def data_length(self): + return get_data_length(self._bytes) + + @property + def address(self): + return get_address(self._bytes) + + +class RMAPRequestReplyMixin(SpaceWirePacket): + @property + def target_address(self): + """Returns the target logical address.""" + return self._bytes[4] + + @property + def status(self): + """Returns the status field.""" + return self._bytes[3] class RMAPPacket(SpaceWirePacket): @@ -1052,11 +734,15 @@

        Module egse.spw

        super().__init__(data) def __str__(self): - return ( - f"{self.__class__.__name__}:\n" - f" Logical Address = 0x{self.logical_address:02X}\n" - f" Data = {self.data}\n" + msg = ( + f"RMAP Base Packet ({len(self)} bytes)\n" + f"Logical address: 0x{self.logical_address:02x}\n" + f"Protocol ID: 0x{self.protocol_id:02x}\n" + f"Instruction: 0x{self.instruction:02x} (0o{self.instruction:08b})\n" + f"Transaction ID: 0x{self.transaction_id:04x} ({self.transaction_id})\n" + f"Data = {self._bytes}\n" ) + return msg @property def instruction(self): @@ -1066,6 +752,14 @@

        Module egse.spw

        def transaction_id(self): return get_transaction_identifier(self._bytes) + @property + def header_crc(self): + return get_header_crc(self._bytes) + + @property + def data_crc(self): + return get_data_crc(self._bytes) + @classmethod def is_rmap_packet(cls, data: Union[bytes, np.ndarray]): if data[1] == 0x01: # Protocol ID @@ -1073,7 +767,7 @@

        Module egse.spw

        return False -class WriteRequest(RMAPPacket): +class WriteRequest(RMAPPacket, RMAPRequestMixin): """A Write Request SpaceWire RMAP Packet.""" def __init__(self, data: Union[bytes, np.ndarray]): @@ -1085,10 +779,6 @@

        Module egse.spw

        def is_unverified(self): return self._bytes[2] == 0x6C - @property - def address(self): - return get_address(self._bytes) - @property def data_length(self): return get_data_length(self._bytes) @@ -1109,15 +799,36 @@

        Module egse.spw

        def __str__(self): prefix = "Verified" if self.is_verified() else "Unverified" - return f"{prefix} Write Request: {self.transaction_id=}, data=0x{self.data.hex()}" + limit = 32 + data_hex = ' '.join(f'{x:02X}' for x in self.data[:limit]) + data_hex += ' ...' if len(self.data) > limit else '' + data_asc = ''.join((chr(byte) if 32 <= byte <= 126 else '.') for byte in self.data[:limit]) + data_asc += ' ...' if len(self.data) > limit else '' + + packet = self.packet_as_bytes + msg = ( + f"RMAP {prefix} Write Request ({len(packet)} bytes)\n" + f"Target address: 0x{self.logical_address:02x}\n" + f"Protocol ID: 0x{self.protocol_id:02x}\n" + f"Instruction: 0x{self.instruction:02x} (0o{self.instruction:08b})\n" + f"Key: 0x{self.key:02x}\n" + f"Initiator address: 0x{self.initiator_address:02x}\n" + f"Transaction ID: 0x{self.transaction_id:04x} ({self.transaction_id})\n" + f"Address: 0x{self.address:08x}\n" + f"Data Length: {self.data_length}\n" + f"Header CRC: 0x{self.header_crc:02x}\n" + f"data (hex): 0x{data_hex}\n" + f"data (ascii): {data_asc}\n" + f"Data CRC: 0x{self.data_crc:02x}\n" + ) + return msg -class WriteRequestReply(RMAPPacket): +class WriteRequestReply(RMAPPacket, RMAPRequestReplyMixin): """An RMAP Reply packet to a Write Request.""" def __init__(self, data: Union[bytes, np.ndarray]): super().__init__(data) - self._status = data[3] @classmethod def is_write_reply(cls, data: Union[bytes, np.ndarray]): @@ -1128,15 +839,22 @@

        Module egse.spw

        if (data[2] == 0x3C or data[2] == 0x2C) and data[4] == 0x51: return True - @property - def status(self): - return self._status - def __str__(self): - return f"Write Request Reply: status={self.status}" + msg = ( + f"Write Request Reply ({len(self)} bytes)\n" + f"Initiator address: 0x{self.logical_address:02x}\n" + f"Protocol ID: 0x{self.protocol_id:02x}\n" + f"Instruction: 0x{self.instruction:02x} (0o{self.instruction:08b})\n" + f"Status: 0x{self.status:02x}\n" + f"target address: 0x{self.target_address:02x}\n" + f"transaction ID: 0x{self.transaction_id:02x} ({self.transaction_id})\n" + f"Header CRC: 0x{self.header_crc:02x}\n" + ) + + return msg -class ReadRequest(RMAPPacket): +class ReadRequest(RMAPPacket, RMAPRequestMixin): """A Read Request SpaceWire RMAP Packet.""" def __init__(self, data: Union[bytes, np.ndarray]): @@ -1152,22 +870,24 @@

        Module egse.spw

        return True return False - @property - def address(self): - return get_address(self._bytes) - - @property - def data_length(self): - return get_data_length(self._bytes) - def __str__(self): - return ( - f"Read Request: tid={self.transaction_id}, address=0x{self.address:04x}, " - f"data length={self.data_length}" + msg = ( + f"RMAP Read Request ({len(self)} bytes)\n" + f"Target address: 0x{self.logical_address:02x}\n" + f"Protocol ID: 0x{self.protocol_id:02x}\n" + f"Instruction: 0x{self.instruction:02x} (0o{self.instruction:08b})\n" + f"Key: 0x{self.key:02x}\n" + f"Initiator address: 0x{self.initiator_address:02x}\n" + f"Transaction ID: 0x{self.transaction_id:04x} ({self.transaction_id})\n" + f"(Extended) Address: 0x{self.address:08x}\n" + f"Data Length: {self.data_length}\n" + f"Header CRC: 0x{self.header_crc:02x}\n" ) + return msg + -class ReadRequestReply(RMAPPacket): +class ReadRequestReply(RMAPPacket, RMAPRequestReplyMixin): """An RMAP Reply packet to a Read Request.""" def __init__(self, data: Union[bytes, np.ndarray]): @@ -1192,8 +912,28 @@

        Module egse.spw

        def __str__(self): data_length = self.data_length - return f"Read Request Reply: data length={data_length}, data={self.data[:20]} " \ - f"{'(data is cut to max 20 bytes)' if data_length > 20 else ''}\n" + limit = 32 + data_hex = ' '.join(f'{x:02X}' for x in self.data[:limit]) + data_hex += ' ...' if len(self.data) > limit else '' + data_asc = ''.join((chr(byte) if 32 <= byte <= 126 else '.') for byte in self.data[:limit]) + data_asc += ' ...' if len(self.data) > limit else '' + + msg = ( + f"Read Request Reply ({len(self)} bytes)\n" + f"Initiator address: 0x{self.logical_address:02x}\n" + f"Protocol ID: 0x{self.protocol_id:02x}\n" + f"Instruction: 0x{self.instruction:02x} (0o{self.instruction:08b})\n" + f"Status: 0x{self.status:02x}\n" + f"target address: 0x{self.target_address:02x}\n" + f"transaction ID: 0x{self.transaction_id:02x} ({self.transaction_id})\n" + f"Data Length: {self.data_length}\n" + f"Header CRC: 0x{self.header_crc:02x}\n" + f"Data (hex): 0x{data_hex}\n" + f"Data (ascii): {data_asc}\n" + f"Data CRC: 0x{self.data_crc:02x}\n" + ) + + return msg class SpaceWireInterface: @@ -1204,6 +944,7 @@

        Module egse.spw

        def __enter__(self): self.connect() + return self def __exit__(self, exc_type, exc_val, exc_tb): self.disconnect() @@ -1221,6 +962,7 @@

        Module egse.spw

        raise NotImplementedError def send_timecode(self, timecode: int): + """Send a timecode over the transport layer.""" raise NotImplementedError def read_packet(self, timeout: int = None) -> Tuple[int, bytes]: @@ -1298,11 +1040,6 @@

        Module egse.spw

        # General RMAP helper functions ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -def rmap_crc_check(data, start, length) -> int: - """Calculate the checksum for the given data.""" - return crc_calc(data, start, length) - - def get_protocol_id(data: bytes) -> int: """ Returns the protocol identifier field. The protocol ID is 1 (0x01) for the RMAP protocol. @@ -1310,6 +1047,13 @@

        Module egse.spw

        return data[1] +def is_rmap(rx_buffer): + """ + Returns True if the buffer represents an RMAP packet, False otherwise. + """ + return get_protocol_id(rx_buffer) == egse.rmap.RMAP_PROTOCOL_ID + + def get_reply_address_field_length(rx_buffer) -> int: """Returns the size of reply address field. @@ -1329,9 +1073,15 @@

        Module egse.spw

        Return the data from the RMAP packet. Raises: - ValueError: if there is no data section in the packet (TODO: not yet implemented) + ValueError: if there is no data section in the packet. """ instruction_field = get_instruction_field(rxbuf) + + if is_write(instruction_field) and is_reply(instruction_field): + raise ValueError("A WriteRequestReply packet doesn't contain a data section.") + elif is_read(instruction_field) and is_command(instruction_field): + raise ValueError("A ReadRequest packet doesn't contain a data section.") + address_length = get_reply_address_field_length(rxbuf) data_length = get_data_length(rxbuf) @@ -1341,6 +1091,14 @@

        Module egse.spw

        def check_data_crc(rxbuf): + """ + Verifies that the data CRC that is given in the packet matches the calculated data CRC. + + The only packets that have a data CRC are: WriteRequest, ReadRequestReply, and an F-CAM DataPacket. + + Raises: + A CheckError when the provide and calculated CRC do not match. + """ instruction_field = get_instruction_field(rxbuf) address_length = get_reply_address_field_length(rxbuf) data_length = get_data_length(rxbuf) @@ -1349,15 +1107,40 @@

        Module egse.spw

        idx = offset + address_length d_crc = rxbuf[idx + data_length] - c_crc = rmap_crc_check(rxbuf, idx, data_length) & 0xFF + c_crc = crc_calc(rxbuf, idx, data_length) & 0xFF if d_crc != c_crc: raise CheckError( - f"Data CRC doesn't match calculated CRC, d_crc=0x{d_crc:02X} & c_crc=0x{c_crc:02X}", - RMAP_GENERAL_ERROR + f"Data CRC doesn't match calculated CRC, d_crc=0x{d_crc:02X} & c_crc=0x{c_crc:02X}" ) +def get_data_crc(rxbuf): + """ + Returns the data CRC of the RMAP packet. + + The only packets that have a data CRC are: WriteRequest, ReadRequestReply, and an F-CAM DataPacket. + """ + instruction_field = get_instruction_field(rxbuf) + address_length = get_reply_address_field_length(rxbuf) + data_length = get_data_length(rxbuf) + + offset = 12 if is_read(instruction_field) else 16 + idx = offset + address_length + + d_crc = rxbuf[idx + data_length] + + return d_crc + + def check_header_crc(rxbuf): + """ + Verifies that the header CRC that is given in the packet matches the calculated header CRC. + + Only RMAP Request and RequestReply packets have a header CRC, data packets not. + + Raises: + A CheckError when the provide and calculated CRC do not match. + """ instruction_field = get_instruction_field(rxbuf) if is_command(instruction_field): offset = 15 @@ -1368,17 +1151,37 @@

        Module egse.spw

        idx = offset + get_reply_address_field_length(rxbuf) h_crc = rxbuf[idx] - c_crc = rmap_crc_check(rxbuf, 0, idx) + c_crc = crc_calc(rxbuf, 0, idx) if h_crc != c_crc: raise CheckError( - f"Header CRC doesn't match calculated CRC, h_crc=0x{h_crc:02X} & c_crc=0x{c_crc:02X}", - RMAP_GENERAL_ERROR + f"Header CRC doesn't match calculated CRC, h_crc=0x{h_crc:02X} & c_crc=0x{c_crc:02X}" ) +def get_header_crc(rxbuf): + """ + Returns the Header CRC of the RMAP packet. + + Only RMAP Request and RequestReply packets have a header CRC, data packets not. + """ + instruction_field = get_instruction_field(rxbuf) + if is_command(instruction_field): + offset = 15 + elif is_write(instruction_field): + offset = 7 + else: + offset = 11 + + idx = offset + get_reply_address_field_length(rxbuf) + h_crc = rxbuf[idx] + + return h_crc + + def get_data_length(rxbuf) -> int: """ - Returns the length of the data in bytes. + Returns the length of the data part of an RMAP Request packet. The returned value + is the number of bytes. Raises: TypeError: when this method is used on a Write Request Reply packet (which has no @@ -1394,9 +1197,10 @@

        Module egse.spw

        idx = offset + get_reply_address_field_length(rxbuf) # We could use two alternative decoding methods here: - # int.from_bytes(rxbuf[idx:idx+3], byteorder='big') (timeit=1.166s) - # struct.unpack('>L', b'\x00' + rxbuf[idx:idx+3])[0] (timeit=0.670s) - data_length = struct.unpack('>L', b'\x00' + rxbuf[idx:idx + 3])[0] + + # data_length = int.from_bytes(rxbuf[idx:idx+3], byteorder='big') # (timeit=1.166s) + data_length = struct.unpack('>L', b'\x00' + rxbuf[idx:idx + 3])[0] # (timeit=0.670s) + return data_length @@ -1423,25 +1227,143 @@

        Module egse.spw

        def get_instruction_field(rxbuf): + """ + Returns the instruction field of the RMAP packet. + """ idx = 2 return rxbuf[idx] +def get_key_field(rxbuf): + """ + Returns the 'Key' field of the RMAP packet. + """ + idx = 3 + return rxbuf[idx] + + +def check_instruction(rx_buffer) -> None: + """ + Check the instruction field for inconsistencies and report the values in the LOGGER at DEBUG + level. + + + Args: + rx_buffer (bytes): The read buffer which contains the SpW packet + + Raises: + CheckError: when the reserved bit is not zero, + + Returns: + None. + """ + from egse.rmap import RMAP_NOT_IMPLEMENTED_AUTHORISED + + # The Instruction Field is the third byte (base=0) of the packet buffer. + # Description of the Instruction Field can be found in ECSS-E-ST-50-52C. + + instruction = get_instruction_field(rx_buffer) + if is_reserved(instruction): + raise CheckError( + f"Instruction field [{instruction:08b}] reserved bit is not 0x00", + RMAP_NOT_IMPLEMENTED_AUTHORISED + ) + + msg = "RMAP Instruction Field: " + msg += "Command; " if is_command(instruction) else "Reply; " + msg += "write; " if is_write(instruction) else "read; " + msg += "verify; " if is_verify(instruction) else "don't verify; " + msg += "reply; " if is_reply_required(instruction) else "don't reply; " + msg += "increment; " if is_increment(instruction) else "no increment; " + + MODULE_LOGGER.debug(msg) + if reply_address_length(instruction): + MODULE_LOGGER.debug(f"Reply address length = {reply_address_length(instruction)} bytes.") + + +def check_protocol_id(rxbuf): + from egse.rmap import RMAP_PROTOCOL_ID, RMAP_GENERAL_ERROR + + idx = 1 + protocol_id = rxbuf[idx] + if protocol_id != RMAP_PROTOCOL_ID: + raise CheckError( + f"Protocol id is not the expected value {protocol_id}, expected {RMAP_PROTOCOL_ID}", + RMAP_GENERAL_ERROR) + + +def get_target_logical_address(rxbuf: bytes) -> int: + """ + The target logical address is always the FEE, i.e. 0x50. The location of the target logical + address is different for Request and RequestReply packets. + """ + instruction = get_instruction_field(rxbuf) + offset = 0 if is_command(instruction) else 4 + tla_idx = offset + get_reply_address_field_length(rxbuf) + tla_rxbuf = rxbuf[tla_idx] + return tla_rxbuf + + +def check_target_logical_address(rxbuf, tla): + from egse.rmap import RMAP_GENERAL_ERROR + + tla_rxbuf = get_target_logical_address(rxbuf) + if tla != tla_rxbuf: + raise CheckError( + f"Target Logical Address doesn't match, tla=0x{tla:02X} & rxbuf[0]=0x{tla_rxbuf:02X}", + RMAP_GENERAL_ERROR + ) + + +def get_initiator_logical_address(rxbuf): + """ + The initiator logical address is always the DPU, i.e. 0x51. The location of the initiator logical + address is different for Request and RequestReply packets. + """ + instruction = get_instruction_field(rxbuf) + offset = 4 if is_command(instruction) else 0 + idx = offset + get_reply_address_field_length(rxbuf) + ila_rxbuf = rxbuf[idx] + return ila_rxbuf + + +def check_initiator_logical_address(rxbuf, ila): + ila_rxbuf = get_initiator_logical_address(rxbuf) + if ila != ila_rxbuf: + raise CheckError( + f"Initiator Logical Address doesn't match, ila=0x{ila:02X} & ila_rxbuf=0x" + f"{ila_rxbuf:02X}", + egse.rmap.RMAP_GENERAL_ERROR + ) + + def get_transaction_identifier(rxbuf): idx = 5 + get_reply_address_field_length(rxbuf) tid = struct.unpack('>h', rxbuf[idx:idx + 2])[0] return tid +def check_key(rxbuf, key): + from egse.rmap import RMAP_INVALID_KEY + + idx = 3 + key_rxbuf = rxbuf[idx] + if key != key_rxbuf: + raise CheckError( + f"Key doesn't match, key={key} & key_rxbuf={key_rxbuf}", RMAP_INVALID_KEY + ) + + # Functions to interpret the Instrument Field def is_reserved(instruction): - """The reserved bit of the 2-bit packet type field from the instruction field. + """ + The reserved bit of the 2-bit packet type field from the instruction field. For PLATO this bit shall be zero as the 0b10 and 0b11 packet field values are reserved. Returns: - bit value: 1 or 0. + The bit value: 1 or 0. """ return (instruction & 0b10000000) >> 7 @@ -1477,7 +1399,7 @@

        Module egse.spw

        Args: instruction (int): the instruction field of an RMAP packet - .. note:: the name of this function might be confusing. + Note: the name of this function might be confusing. This function does **not** test if the packet is a reply packet, but it checks if the command requests a reply from the target. If you need to test if the @@ -1515,124 +1437,27 @@

        Module egse.spw

        Functions

        -
        -def check_address_and_data_length(address: int, length: int, strict: bool = True) ‑> None +
        +def check_data_crc(rxbuf)
        -

        Checks the address and length in the range of memory areas used by the FEE.

        -

        The ranges are taken from the PLATO-DLR-PL-ICD-0010 N-FEE to N-DPU IRD.

        -

        Args

        -
        -
        address : int
        -
        the memory address of the FEE Register
        -
        length : int
        -
        the number of bytes requested
        -
        strict : bool
        -
        strictly apply the rules
        -
        +

        Verifies that the data CRC that is given in the packet matches the calculated data CRC.

        +

        The only packets that have a data CRC are: WriteRequest, ReadRequestReply, and an F-CAM DataPacket.

        Raises

        -
        -
        RMAPError
        -
        when address + length fall outside any specified area.
        -
        +

        A CheckError when the provide and calculated CRC do not match.

        Expand source code -
        def check_address_and_data_length(address: int, length: int, strict: bool = True) -> None:
        +
        def check_data_crc(rxbuf):
             """
        -    Checks the address and length in the range of memory areas used by the FEE.
        +    Verifies that the data CRC that is given in the packet matches the calculated data CRC.
         
        -    The ranges are taken from the PLATO-DLR-PL-ICD-0010 N-FEE to N-DPU IRD.
        -
        -    Args:
        -        address (int): the memory address of the FEE Register
        -        length (int): the number of bytes requested
        -        strict (bool): strictly apply the rules
        +    The only packets that have a data CRC are: WriteRequest, ReadRequestReply, and an F-CAM DataPacket.
         
             Raises:
        -        RMAPError: when address + length fall outside any specified area.
        +        A CheckError when the provide and calculated CRC do not match.
             """
        -
        -    if not strict:
        -        # All these restrictions have been relaxed on the N-FEE.
        -        # We are returning here immediately instead of removing or commenting out the code.
        -        # These reason is that we can then bring back restriction easier and gradually.
        -
        -        MODULE_LOGGER.warning(
        -            "Address and data length checks have been disabled, because the N-FEE "
        -            "does not enforce restrictions in the critical memory area.")
        -        return
        -
        -    if length % 4:
        -        raise RMAPError(
        -            "The requested data length shall be a multiple of 4 bytes.", address, length
        -        )
        -
        -    if address % 4:
        -        raise RMAPError("The address shall be a multiple of 4 bytes.", address, length)
        -
        -    # Note that when checking the given data length, at the defined area end,
        -    # we can still read 4 bytes.
        -
        -    if CRITICAL_AREA_START <= address <= CRITICAL_AREA_END:
        -        if length != 4:
        -            raise RMAPError(
        -                "Read requests to the critical area have a fixed data length of 4 bytes.",
        -                address, length
        -            )
        -    elif GENERAL_AREA_START <= address <= GENERAL_AREA_END:
        -        if length > 256:
        -            raise RMAPError(
        -                "Read requests to the general area have a maximum data length of 256 bytes.",
        -                address, length
        -            )
        -        if address + length > GENERAL_AREA_END + 4:
        -            raise RMAPError(
        -                "The requested data length for the general area is too large. "
        -                "The address + length exceeds the general area boundaries.",
        -                address, length
        -            )
        -
        -    elif HK_AREA_START <= address <= HK_AREA_END:
        -        if length > 256:
        -            raise RMAPError(
        -                "Read requests to the housekeeping area have a maximum data length of 256 bytes.",
        -                address, length
        -            )
        -        if address + length > HK_AREA_END + 4:
        -            raise RMAPError(
        -                "The requested data length for the housekeeping area is too large. "
        -                "The address + length exceeds the housekeeping area boundaries.",
        -                address, length
        -            )
        -
        -    elif WINDOWING_AREA_START <= address <= WINDOWING_AREA_END:
        -        if length > 4096:
        -            raise RMAPError(
        -                "Read requests to the windowing area have a maximum data length of 4096 bytes.",
        -                address, length
        -            )
        -        if address + length > WINDOWING_AREA_END + 4:
        -            raise RMAPError(
        -                "The requested data length for the windowing area is too large. "
        -                "The address + length exceeds the windowing area boundaries.", address, length
        -            )
        -
        -    else:
        -        raise RMAPError("Register address for RMAP read requests is invalid.", address, length)
        -
        -
        -
        -def check_data_crc(rxbuf) -
        -
        -
        -
        - -Expand source code - -
        def check_data_crc(rxbuf):
             instruction_field = get_instruction_field(rxbuf)
             address_length = get_reply_address_field_length(rxbuf)
             data_length = get_data_length(rxbuf)
        @@ -1641,11 +1466,10 @@ 

        Raises

        idx = offset + address_length d_crc = rxbuf[idx + data_length] - c_crc = rmap_crc_check(rxbuf, idx, data_length) & 0xFF + c_crc = crc_calc(rxbuf, idx, data_length) & 0xFF if d_crc != c_crc: raise CheckError( - f"Data CRC doesn't match calculated CRC, d_crc=0x{d_crc:02X} & c_crc=0x{c_crc:02X}", - RMAP_GENERAL_ERROR + f"Data CRC doesn't match calculated CRC, d_crc=0x{d_crc:02X} & c_crc=0x{c_crc:02X}" )
        @@ -1653,12 +1477,23 @@

        Raises

        def check_header_crc(rxbuf)
        -
        +

        Verifies that the header CRC that is given in the packet matches the calculated header CRC.

        +

        Only RMAP Request and RequestReply packets have a header CRC, data packets not.

        +

        Raises

        +

        A CheckError when the provide and calculated CRC do not match.

        Expand source code
        def check_header_crc(rxbuf):
        +    """
        +    Verifies that the header CRC that is given in the packet matches the calculated header CRC.
        +
        +    Only RMAP Request and RequestReply packets have a header CRC, data packets not.
        +
        +    Raises:
        +        A CheckError when the provide and calculated CRC do not match.
        +    """
             instruction_field = get_instruction_field(rxbuf)
             if is_command(instruction_field):
                 offset = 15
        @@ -1669,373 +1504,135 @@ 

        Raises

        idx = offset + get_reply_address_field_length(rxbuf) h_crc = rxbuf[idx] - c_crc = rmap_crc_check(rxbuf, 0, idx) + c_crc = crc_calc(rxbuf, 0, idx) if h_crc != c_crc: raise CheckError( - f"Header CRC doesn't match calculated CRC, h_crc=0x{h_crc:02X} & c_crc=0x{c_crc:02X}", - RMAP_GENERAL_ERROR + f"Header CRC doesn't match calculated CRC, h_crc=0x{h_crc:02X} & c_crc=0x{c_crc:02X}" )
        -
        -def create_rmap_read_request_packet(address: int, length: int, tid: int, strict: bool = True) ‑> bytes +
        +def check_initiator_logical_address(rxbuf, ila)
        -

        Creates an RMAP Read Request SpaceWire packet.

        -

        The read request is an RMAP command that read a number of bytes from the FEE register memory.

        -

        The function returns a ctypes character array (which is basically a bytes array) that -can be passed into the EtherSpaceLink library function esl_write_packet().

        -

        Address shall be within the 0x0000_0000 and 0x00FF_FFFC. The memory map (register) is divided -in the following areas:

        -
        0x0000_0000 - 0x0000_00FC   Critical Configuration Area (verified write)
        -0x0000_0100 - 0x0000_06FC   General Configuration Area (unverified write)
        -0x0000_0700 - 0x0000_07FC   Housekeeping area
        -0x0000_0800 - 0x007F_FFFC   Not Supported
        -0x0080_0000 - 0x00FF_FFFC   Windowing Area (unverified write)
        -0x0010_0000 - 0xFFFF_FFFC   Not Supported
        -
        -

        All read requests to the critical area shall have a fixed data length of 4 bytes. -All read requests to a general area shall have a maximum data length of 256 bytes. -All read requests to the housekeeping area shall have a maximum data length of 256 bytes. -All read requests to the windowing area shall have a maximum data length of 4096 bytes.

        -

        The transaction identifier shall be incremented for each read request. This shall be done by -the calling function!

        -

        Args

        -
        -
        address : int
        -
        the FEE register memory address
        -
        length : int
        -
        the data length
        -
        tid : int
        -
        transaction identifier
        -
        strict : bool
        -
        perform strict checking of address and length
        -
        -

        Returns

        -

        a bytes object containing the full RMAP Read Request packet.

        +
        Expand source code -
        def create_rmap_read_request_packet(address: int, length: int, tid: int, strict: bool = True) -> bytes:
        -    """
        -    Creates an RMAP Read Request SpaceWire packet.
        -
        -    The read request is an RMAP command that read a number of bytes from the FEE register memory.
        -
        -    The function returns a ``ctypes`` character array (which is basically a bytes array) that
        -    can be passed into the EtherSpaceLink library function ``esl_write_packet()``.
        -
        -    Address shall be within the 0x0000_0000 and 0x00FF_FFFC. The memory map (register) is divided
        -    in the following areas:
        -
        -        0x0000_0000 - 0x0000_00FC   Critical Configuration Area (verified write)
        -        0x0000_0100 - 0x0000_06FC   General Configuration Area (unverified write)
        -        0x0000_0700 - 0x0000_07FC   Housekeeping area
        -        0x0000_0800 - 0x007F_FFFC   Not Supported
        -        0x0080_0000 - 0x00FF_FFFC   Windowing Area (unverified write)
        -        0x0010_0000 - 0xFFFF_FFFC   Not Supported
        -
        -    All read requests to the critical area shall have a fixed data length of 4 bytes.
        -    All read requests to a general area shall have a maximum data length of 256 bytes.
        -    All read requests to the housekeeping area shall have a maximum data length of 256 bytes.
        -    All read requests to the windowing area shall have a maximum data length of 4096 bytes.
        -
        -    The transaction identifier shall be incremented for each read request. This shall be done by
        -    the calling function!
        -
        -    Args:
        -        address (int): the FEE register memory address
        -        length (int): the data length
        -        tid (int): transaction identifier
        -        strict (bool): perform strict checking of address and length
        -
        -    Returns:
        -        a bytes object containing the full RMAP Read Request packet.
        -    """
        -
        -    check_address_and_data_length(address, length, strict=strict)
        -
        -    buf = bytearray(16)
        -
        -    # NOTE: The first bytes would each carry the target SpW address or a destination port,
        -    #       but this is not used for point-to-point connections, so we're safe.
        -
        -    buf[0] = 0x51  # Target N-FEE or F-FEE
        -    buf[1] = 0x01  # RMAP Protocol ID
        -    buf[2] = 0x4C  # Instruction: 0b1001100, RMAP Request, Read, Incrementing address, reply address
        -    buf[3] = 0xD1  # Destination Key
        -    buf[4] = 0x50  # Initiator is always the DPU
        -    buf[5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
        -    buf[6] = tid & 0xFF  # LSB of the Transition ID
        -    buf[7] = 0x00  # Extended address is not used
        -    buf[8] = (address >> 24) & 0xFF  # address (MSB)
        -    buf[9] = (address >> 16) & 0xFF  # address
        -    buf[10] = (address >> 8) & 0xFF  # address
        -    buf[11] = address & 0xFF  # address (LSB)
        -    buf[12] = (length >> 16) & 0xFF  # data length (MSB)
        -    buf[13] = (length >> 8) & 0xFF  # data length
        -    buf[14] = length & 0xFF  # data length (LSB)
        -    buf[15] = rmap_crc_check(buf, 0, 15) & 0xFF
        -    return bytes(buf)
        +
        def check_initiator_logical_address(rxbuf, ila):
        +    ila_rxbuf = get_initiator_logical_address(rxbuf)
        +    if ila != ila_rxbuf:
        +        raise CheckError(
        +            f"Initiator Logical Address doesn't match, ila=0x{ila:02X} & ila_rxbuf=0x"
        +            f"{ila_rxbuf:02X}",
        +            egse.rmap.RMAP_GENERAL_ERROR
        +        )
        -
        -def create_rmap_read_request_reply_packet(instruction_field: int, tid: int, status: int, buffer: bytes, buffer_length: int) ‑> bytes +
        +def check_instruction(rx_buffer) ‑> None
        -

        Creates an RMAP Reply to a RMAP Read Request packet.

        -

        The function returns a ctypes character array (which is basically a bytes array) that -can be passed into the EtherSpaceLink library function esl_write_packet().

        +

        Check the instruction field for inconsistencies and report the values in the LOGGER at DEBUG +level.

        Args

        -
        instruction_field : int
        -
        the instruction field of the RMAP read request packet
        -
        tid : int
        -
        the transaction identifier of the read request packet
        -
        status : int
        -
        the status field, 0 on success
        -
        buffer : bytes
        -
        the data that was read as indicated by the read request
        -
        buffer_length : int
        -
        the data length
        +
        rx_buffer : bytes
        +
        The read buffer which contains the SpW packet
        -

        Returns

        +

        Raises

        -
        packet
        -
        a bytes object containing the full RMAP Reply packet.
        -
        +
        CheckError
        +
        when the reserved bit is not zero,
        +
        +

        Returns

        +

        None.

        Expand source code -
        def create_rmap_read_request_reply_packet(
        -        instruction_field: int, tid: int, status: int, buffer: bytes, buffer_length: int) -> bytes:
        +
        def check_instruction(rx_buffer) -> None:
             """
        -    Creates an RMAP Reply to a RMAP Read Request packet.
        +    Check the instruction field for inconsistencies and report the values in the LOGGER at DEBUG
        +    level.
         
        -    The function returns a ``ctypes`` character array (which is basically a bytes array) that
        -    can be passed into the EtherSpaceLink library function ``esl_write_packet()``.
         
             Args:
        -        instruction_field (int): the instruction field of the RMAP read request packet
        -        tid (int): the transaction identifier of the read request packet
        -        status (int): the status field, 0 on success
        -        buffer (bytes): the data that was read as indicated by the read request
        -        buffer_length (int): the data length
        +        rx_buffer (bytes): The read buffer which contains the SpW packet
        +
        +    Raises:
        +        CheckError: when the reserved bit is not zero,
         
             Returns:
        -        packet: a bytes object containing the full RMAP Reply packet.
        +        None.
             """
        +    from egse.rmap import RMAP_NOT_IMPLEMENTED_AUTHORISED
         
        -    buf = bytearray(12 + buffer_length + 1)
        -
        -    buf[0] = 0x50  # Initiator address N-DPU or F-DPU
        -    buf[1] = 0x01  # RMAP Protocol ID
        -    buf[2] = instruction_field & 0x3F  # Clear the command bit as this is a reply
        -    buf[3] = status & 0xFF  # Status field: 0 on success
        -    buf[4] = 0x51  # Target address is always the N-FEE or F-FEE
        -    buf[5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
        -    buf[6] = tid & 0xFF  # LSB of the Transition ID
        -    buf[7] = 0x00  # Reserved
        -    buf[8] = (buffer_length >> 16) & 0xFF  # data length (MSB)
        -    buf[9] = (buffer_length >> 8) & 0xFF  # data length
        -    buf[10] = buffer_length & 0xFF  # data length (LSB)
        -    buf[11] = rmap_crc_check(buf, 0, 11) & 0xFF  # Header CRC
        -
        -    # Note that we assume here that len(buffer) == buffer_length.
        -
        -    if len(buffer) != buffer_length:
        -        MODULE_LOGGER.warning(
        -            f"While creating an RMAP read reply packet, the length of the buffer ({len(buffer)}) "
        -            f"not equals the buffer_length ({buffer_length})"
        -        )
        +    # The Instruction Field is the third byte (base=0) of the packet buffer.
        +    # Description of the Instruction Field can be found in ECSS-E-ST-50-52C.
         
        -    for idx, value in enumerate(buffer):
        -        buf[12 + idx] = value
        +    instruction = get_instruction_field(rx_buffer)
        +    if is_reserved(instruction):
        +        raise CheckError(
        +            f"Instruction field [{instruction:08b}] reserved bit is not 0x00",
        +            RMAP_NOT_IMPLEMENTED_AUTHORISED
        +        )
         
        -    buf[12 + buffer_length] = rmap_crc_check(buffer, 0, buffer_length) & 0xFF  # data CRC
        +    msg = "RMAP Instruction Field: "
        +    msg += "Command; " if is_command(instruction) else "Reply; "
        +    msg += "write; " if is_write(instruction) else "read; "
        +    msg += "verify; " if is_verify(instruction) else "don't verify; "
        +    msg += "reply; " if is_reply_required(instruction) else "don't reply; "
        +    msg += "increment; " if is_increment(instruction) else "no increment; "
         
        -    return bytes(buf)
        + MODULE_LOGGER.debug(msg) + if reply_address_length(instruction): + MODULE_LOGGER.debug(f"Reply address length = {reply_address_length(instruction)} bytes.")
      -
      -def create_rmap_unverified_write_packet(address: int, data: bytes, length: int, tid: int) ‑> bytes +
      +def check_key(rxbuf, key)
      -

      Create an RMAP packet for a unverified write request on the FEE.

      -

      Args

      -
      -
      address
      -
      the start memory address on the FEE register map
      -
      data
      -
      the data to be written in the register map at address
      -
      length
      -
      the length of the data
      -
      tid : int
      -
      transaction identifier
      -
      -

      Returns

      -
      -
      packet
      -
      a bytes object containing the SpaceWire packet.
      -
      +
      Expand source code -
      def create_rmap_unverified_write_packet(address: int, data: bytes, length: int, tid: int) -> bytes:
      -    """
      -    Create an RMAP packet for a unverified write request on the FEE.
      -
      -    Args:
      -        address: the start memory address on the FEE register map
      -        data: the data to be written in the register map at address
      -        length: the length of the data
      -        tid (int): transaction identifier
      -
      -    Returns:
      -        packet: a bytes object containing the SpaceWire packet.
      -    """
      -
      -    # We can only handle data for which the length >= the given length argument.
      +
      def check_key(rxbuf, key):
      +    from egse.rmap import RMAP_INVALID_KEY
       
      -    if len(data) < length:
      -        raise ValueError(
      -            f"The length of the data argument ({len(data)}) is smaller than "
      -            f"the given length argument ({length})."
      -        )
      -
      -    if len(data) > length:
      -        MODULE_LOGGER.warning(
      -            f"The length of the data argument ({len(data)}) is larger than "
      -            f"the given length argument ({length}). The data will be truncated "
      -            f"when copied into the packet."
      -        )
      -
      -    if address <= CRITICAL_AREA_END:
      -        raise ValueError(
      -            f"The given address (0x{address:08X}) is in the range for critical configuration is "
      -            f"[0x00 - 0xFC]. Use the verified write function for this."
      -        )
      -
      -    tid = update_transaction_identifier(tid)
      -
      -    # Buffer length is fixed at 24 bytes since the data length
      -    # is fixed at 4 bytes (32 bit addressing)
      -
      -    buf = bytearray(16 + length + 1)
      -    offset = 0
      -
      -    buf[offset + 0] = 0x51  # Logical Address
      -    buf[offset + 1] = 0x01  # Protocol ID
      -    buf[offset + 2] = 0x6C  # Instruction
      -    buf[offset + 3] = 0xD1  # Key
      -    buf[offset + 4] = 0x50  # Initiator Address
      -    buf[offset + 5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
      -    buf[offset + 6] = tid & 0xFF  # LSB of the Transition ID
      -    buf[offset + 7] = 0x00  # Extended address
      -    buf[offset + 8] = (address >> 24) & 0xFF  # address (MSB)
      -    buf[offset + 9] = (address >> 16) & 0xFF  # address
      -    buf[offset + 10] = (address >> 8) & 0xFF  # address
      -    buf[offset + 11] = address & 0xFF  # address (LSB)
      -    buf[offset + 12] = (length >> 16) & 0xFF  # data length (MSB)
      -    buf[offset + 13] = (length >> 8) & 0xFF  # data length
      -    buf[offset + 14] = length & 0xFF  # data length (LSB)
      -    buf[offset + 15] = rmap_crc_check(buf, 0, 15) & 0xFF  # header CRC
      -
      -    offset += 16
      -
      -    for idx, value in enumerate(data):
      -        buf[offset + idx] = value
      -
      -    buf[offset + length] = rmap_crc_check(buf, offset, length) & 0xFF  # data CRC
      -
      -    return bytes(buf)
      + idx = 3 + key_rxbuf = rxbuf[idx] + if key != key_rxbuf: + raise CheckError( + f"Key doesn't match, key={key} & key_rxbuf={key_rxbuf}", RMAP_INVALID_KEY + )
      -
      -def create_rmap_verified_write_packet(address: int, data: bytes, tid: int) ‑> bytes +
      +def check_protocol_id(rxbuf)
      -

      Create an RMAP packet for a verified write request on the FEE. The length of the data is -by convention always 4 bytes and therefore not passed as an argument.

      -

      Args

      -
      -
      address
      -
      the start memory address on the FEE register map
      -
      data
      -
      the data to be written in the register map at address [4 bytes]
      -
      tid : int
      -
      transaction identifier
      -
      -

      Returns

      -
      -
      packet
      -
      a bytes object containing the SpaceWire packet.
      -
      +
      Expand source code -
      def create_rmap_verified_write_packet(address: int, data: bytes, tid: int) -> bytes:
      -    """
      -    Create an RMAP packet for a verified write request on the FEE. The length of the data is
      -    by convention always 4 bytes and therefore not passed as an argument.
      -
      -    Args:
      -        address: the start memory address on the FEE register map
      -        data: the data to be written in the register map at address [4 bytes]
      -        tid (int): transaction identifier
      -
      -    Returns:
      -        packet: a bytes object containing the SpaceWire packet.
      -    """
      +
      def check_protocol_id(rxbuf):
      +    from egse.rmap import RMAP_PROTOCOL_ID, RMAP_GENERAL_ERROR
       
      -    if len(data) < 4:
      -        raise ValueError(
      -            f"The data argument should be at least 4 bytes, but it is only {len(data)} bytes: {data=}.")
      -
      -    if address > CRITICAL_AREA_END:
      -        raise ValueError("The address range for critical configuration is [0x00 - 0xFC].")
      -
      -    tid = update_transaction_identifier(tid)
      -
      -    # Buffer length is fixed at 24 bytes since the data length is fixed
      -    # at 4 bytes (32 bit addressing)
      -
      -    buf = bytearray(21)
      -
      -    # The values below are taken from the PLATO N-FEE to N-DPU
      -    # Interface Requirements Document [PLATO-DLR-PL-ICD-0010]
      -
      -    buf[0] = 0x51  # Logical Address
      -    buf[1] = 0x01  # Protocol ID
      -    buf[2] = 0x7C  # Instruction
      -    buf[3] = 0xD1  # Key
      -    buf[4] = 0x50  # Initiator Address
      -    buf[5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
      -    buf[6] = tid & 0xFF  # LSB of the Transition ID
      -    buf[7] = 0x00  # Extended address
      -    buf[8] = (address >> 24) & 0xFF  # address (MSB)
      -    buf[9] = (address >> 16) & 0xFF  # address
      -    buf[10] = (address >> 8) & 0xFF  # address
      -    buf[11] = address & 0xFF  # address (LSB)
      -    buf[12] = 0x00  # data length (MSB)
      -    buf[13] = 0x00  # data length
      -    buf[14] = 0x04  # data length (LSB)
      -    buf[15] = rmap_crc_check(buf, 0, 15) & 0xFF  # header CRC
      -    buf[16] = data[0]
      -    buf[17] = data[1]
      -    buf[18] = data[2]
      -    buf[19] = data[3]
      -    buf[20] = rmap_crc_check(buf, 16, 4) & 0xFF  # data CRC
      -
      -    return bytes(buf)
      + idx = 1 + protocol_id = rxbuf[idx] + if protocol_id != RMAP_PROTOCOL_ID: + raise CheckError( + f"Protocol id is not the expected value {protocol_id}, expected {RMAP_PROTOCOL_ID}", + RMAP_GENERAL_ERROR)
      -
      -def create_rmap_write_request_reply_packet(instruction_field: int, tid: int, status: int) ‑> bytes +
      +def check_target_logical_address(rxbuf, tla)
      @@ -2043,19 +1640,15 @@

      Returns

      Expand source code -
      def create_rmap_write_request_reply_packet(instruction_field: int, tid: int, status: int) -> bytes:
      -    buf = bytearray(8)
      -
      -    buf[0] = 0x50  # Initiator address N-DPU or F-DPU
      -    buf[1] = 0x01  # RMAP Protocol ID
      -    buf[2] = instruction_field & 0x3F  # Clear the command bit as this is a reply
      -    buf[3] = status & 0xFF  # Status field: 0 on success
      -    buf[4] = 0x51  # Target address is always the N-FEE or F-FEE
      -    buf[5] = (tid >> 8) & 0xFF  # MSB of the Transition ID
      -    buf[6] = tid & 0xFF  # LSB of the Transition ID
      -    buf[7] = rmap_crc_check(buf, 0, 7) & 0xFF  # Header CRC
      -
      -    return bytes(buf)
      +
      def check_target_logical_address(rxbuf, tla):
      +    from egse.rmap import RMAP_GENERAL_ERROR
      +
      +    tla_rxbuf = get_target_logical_address(rxbuf)
      +    if tla != tla_rxbuf:
      +        raise CheckError(
      +            f"Target Logical Address doesn't match, tla=0x{tla:02X} & rxbuf[0]=0x{tla_rxbuf:02X}",
      +            RMAP_GENERAL_ERROR
      +        )
      @@ -2102,7 +1695,7 @@

      Raises

      Raises

      ValueError
      -
      if there is no data section in the packet (TODO: not yet implemented)
      +
      if there is no data section in the packet.
      @@ -2113,9 +1706,15 @@

      Raises

      Return the data from the RMAP packet. Raises: - ValueError: if there is no data section in the packet (TODO: not yet implemented) + ValueError: if there is no data section in the packet. """ instruction_field = get_instruction_field(rxbuf) + + if is_write(instruction_field) and is_reply(instruction_field): + raise ValueError("A WriteRequestReply packet doesn't contain a data section.") + elif is_read(instruction_field) and is_command(instruction_field): + raise ValueError("A ReadRequest packet doesn't contain a data section.") + address_length = get_reply_address_field_length(rxbuf) data_length = get_data_length(rxbuf) @@ -2124,11 +1723,40 @@

      Raises

      return rxbuf[offset + address_length:offset + address_length + data_length]
    +
    +def get_data_crc(rxbuf) +
    +
    +

    Returns the data CRC of the RMAP packet.

    +

    The only packets that have a data CRC are: WriteRequest, ReadRequestReply, and an F-CAM DataPacket.

    +
    + +Expand source code + +
    def get_data_crc(rxbuf):
    +    """
    +    Returns the data CRC of the RMAP packet.
    +
    +    The only packets that have a data CRC are: WriteRequest, ReadRequestReply, and an F-CAM DataPacket.
    +    """
    +    instruction_field = get_instruction_field(rxbuf)
    +    address_length = get_reply_address_field_length(rxbuf)
    +    data_length = get_data_length(rxbuf)
    +
    +    offset = 12 if is_read(instruction_field) else 16
    +    idx = offset + address_length
    +
    +    d_crc = rxbuf[idx + data_length]
    +
    +    return d_crc
    +
    +
    def get_data_length(rxbuf) ‑> int
    -

    Returns the length of the data in bytes.

    +

    Returns the length of the data part of an RMAP Request packet. The returned value +is the number of bytes.

    Raises

    TypeError
    @@ -2139,44 +1767,118 @@

    Raises

    Expand source code -
    def get_data_length(rxbuf) -> int:
    +
    def get_data_length(rxbuf) -> int:
    +    """
    +    Returns the length of the data part of an RMAP Request packet. The returned value
    +    is the number of bytes.
    +
    +    Raises:
    +        TypeError: when this method is used on a Write Request Reply packet (which has no
    +            data length).
    +    """
    +    instruction_field = get_instruction_field(rxbuf)
    +
    +    if not is_command(instruction_field) and is_write(instruction_field):
    +        raise TypeError("There is no data length field for Write Request Reply packets, "
    +                        "asking for the data length is an invalid operation.")
    +
    +    offset = 12 if is_command(instruction_field) else 8
    +    idx = offset + get_reply_address_field_length(rxbuf)
    +
    +    # We could use two alternative decoding methods here:
    +
    +    # data_length = int.from_bytes(rxbuf[idx:idx+3], byteorder='big')  # (timeit=1.166s)
    +    data_length = struct.unpack('>L', b'\x00' + rxbuf[idx:idx + 3])[0]  # (timeit=0.670s)
    +
    +    return data_length
    + +
    +
    +def get_header_crc(rxbuf) +
    +
    +

    Returns the Header CRC of the RMAP packet.

    +

    Only RMAP Request and RequestReply packets have a header CRC, data packets not.

    +
    + +Expand source code + +
    def get_header_crc(rxbuf):
    +    """
    +    Returns the Header CRC of the RMAP packet.
    +
    +    Only RMAP Request and RequestReply packets have a header CRC, data packets not.
    +    """
    +    instruction_field = get_instruction_field(rxbuf)
    +    if is_command(instruction_field):
    +        offset = 15
    +    elif is_write(instruction_field):
    +        offset = 7
    +    else:
    +        offset = 11
    +
    +    idx = offset + get_reply_address_field_length(rxbuf)
    +    h_crc = rxbuf[idx]
    +
    +    return h_crc
    +
    +
    +
    +def get_initiator_logical_address(rxbuf) +
    +
    +

    The initiator logical address is always the DPU, i.e. 0x51. The location of the initiator logical +address is different for Request and RequestReply packets.

    +
    + +Expand source code + +
    def get_initiator_logical_address(rxbuf):
         """
    -    Returns the length of the data in bytes.
    -
    -    Raises:
    -        TypeError: when this method is used on a Write Request Reply packet (which has no
    -            data length).
    +    The initiator logical address is always the DPU, i.e. 0x51. The location of the initiator logical
    +    address is different for Request and RequestReply packets.
         """
    -    instruction_field = get_instruction_field(rxbuf)
    -
    -    if not is_command(instruction_field) and is_write(instruction_field):
    -        raise TypeError("There is no data length field for Write Request Reply packets, "
    -                        "asking for the data length is an invalid operation.")
    -
    -    offset = 12 if is_command(instruction_field) else 8
    +    instruction = get_instruction_field(rxbuf)
    +    offset = 4 if is_command(instruction) else 0
         idx = offset + get_reply_address_field_length(rxbuf)
    -
    -    # We could use two alternative decoding methods here:
    -    #   int.from_bytes(rxbuf[idx:idx+3], byteorder='big')    (timeit=1.166s)
    -    #   struct.unpack('>L', b'\x00' + rxbuf[idx:idx+3])[0]   (timeit=0.670s)
    -    data_length = struct.unpack('>L', b'\x00' + rxbuf[idx:idx + 3])[0]
    -    return data_length
    + ila_rxbuf = rxbuf[idx] + return ila_rxbuf
    def get_instruction_field(rxbuf)
    -
    +

    Returns the instruction field of the RMAP packet.

    Expand source code
    def get_instruction_field(rxbuf):
    +    """
    +    Returns the instruction field of the RMAP packet.
    +    """
         idx = 2
         return rxbuf[idx]
    +
    +def get_key_field(rxbuf) +
    +
    +

    Returns the 'Key' field of the RMAP packet.

    +
    + +Expand source code + +
    def get_key_field(rxbuf):
    +    """
    +    Returns the 'Key' field of the RMAP packet.
    +    """
    +    idx = 3
    +    return rxbuf[idx]
    +
    +
    def get_protocol_id(data: bytes) ‑> int
    @@ -2224,6 +1926,28 @@

    Returns

    return reply_address_length(instruction) * 4
    +
    +def get_target_logical_address(rxbuf: bytes) ‑> int +
    +
    +

    The target logical address is always the FEE, i.e. 0x50. The location of the target logical +address is different for Request and RequestReply packets.

    +
    + +Expand source code + +
    def get_target_logical_address(rxbuf: bytes) -> int:
    +    """
    +    The target logical address is always the FEE, i.e. 0x50. The location of the target logical
    +    address is different for Request and RequestReply packets.
    +    """
    +    instruction = get_instruction_field(rxbuf)
    +    offset = 0 if is_command(instruction) else 4
    +    tla_idx = offset + get_reply_address_field_length(rxbuf)
    +    tla_rxbuf = rxbuf[tla_idx]
    +    return tla_rxbuf
    +
    +
    def get_transaction_identifier(rxbuf)
    @@ -2305,12 +2029,11 @@

    Args

    instruction : int
    the instruction field of an RMAP packet
    -
    -

    Note: the name of this function might be confusing.

    -

    This function does not test if the packet is a reply packet, but it checks +

    Note: the name of this function might be confusing.

    +
    This function does **not** test if the packet is a reply packet, but it checks
     if the command requests a reply from the target. If you need to test if the
    -packet is a command or a reply, use the is_command() or is_reply() function.

    -
    +packet is a command or a reply, use the is_command() or is_reply() function. +
    Expand source code @@ -2321,7 +2044,7 @@

    Args

    Args: instruction (int): the instruction field of an RMAP packet - .. note:: the name of this function might be confusing. + Note: the name of this function might be confusing. This function does **not** test if the packet is a reply packet, but it checks if the command requests a reply from the target. If you need to test if the @@ -2339,7 +2062,7 @@

    Args

    For PLATO this bit shall be zero as the 0b10 and 0b11 packet field values are reserved.

    Returns

    -
    bit value
    +
    The bit value
    1 or 0.
    @@ -2347,16 +2070,33 @@

    Returns

    Expand source code
    def is_reserved(instruction):
    -    """The reserved bit of the 2-bit packet type field from the instruction field.
    +    """
    +    The reserved bit of the 2-bit packet type field from the instruction field.
     
         For PLATO this bit shall be zero as the 0b10 and 0b11 packet field values are reserved.
     
         Returns:
    -        bit value: 1 or 0.
    +        The bit value: 1 or 0.
         """
         return (instruction & 0b10000000) >> 7
    +
    +def is_rmap(rx_buffer) +
    +
    +

    Returns True if the buffer represents an RMAP packet, False otherwise.

    +
    + +Expand source code + +
    def is_rmap(rx_buffer):
    +    """
    +    Returns True if the buffer represents an RMAP packet, False otherwise.
    +    """
    +    return get_protocol_id(rx_buffer) == egse.rmap.RMAP_PROTOCOL_ID
    +
    +
    def is_verify(instruction)
    @@ -2418,20 +2158,6 @@

    Returns

    return (instruction & 0b00000011) << 2
    -
    -def rmap_crc_check(data, start, length) ‑> int -
    -
    -

    Calculate the checksum for the given data.

    -
    - -Expand source code - -
    def rmap_crc_check(data, start, length) -> int:
    -    """Calculate the checksum for the given data."""
    -    return crc_calc(data, start, length)
    -
    -
    def to_string(data: DataPacketType) ‑> str
    @@ -2489,9 +2215,10 @@

    Args

    Updates the transaction identifier and returns the new value.

    -

    FIXME: document more about this identifier, where is it used, when is it checked, -when does it need to be incremented, who initializes the identifier and -who updates it, …

    +

    This identifier shall be incremented for each RMAP Request. The RMAP Request Reply packets +shall copy the transaction ID of the RMAP Request packet in their transaction ID field.

    +

    The transaction ID is a 16-bit field which is used to associate replies with the command +that caused the reply.

    Args

    tid : int
    @@ -2507,9 +2234,11 @@

    Returns

    """ Updates the transaction identifier and returns the new value. - FIXME: document more about this identifier, where is it used, when is it checked, - when does it need to be incremented, who initializes the identifier and - who updates it, ... + This identifier shall be incremented for each RMAP Request. The RMAP Request Reply packets + shall copy the transaction ID of the RMAP Request packet in their transaction ID field. + + The transaction ID is a 16-bit field which is used to associate replies with the command + that caused the reply. Args: tid (int): The current transaction identifier @@ -2531,14 +2260,14 @@

    Classes

    (message, status)
    -

    Raised when a check fails and you want to pass a status values along with the message.

    +

    Raised when a check fails, and you want to pass a status values along with the message.

    Expand source code -
    class CheckError(RMAPError):
    +
    class CheckError(Error):
         """
    -    Raised when a check fails and you want to pass a status values along with the message.
    +    Raised when a check fails, and you want to pass a status values along with the message.
         """
     
         def __init__(self, message, status):
    @@ -2547,7 +2276,6 @@ 

    Classes

    Ancestors

      -
    • RMAPError
    • Error
    • CommonEGSEException
    • builtins.Exception
    • @@ -2786,6 +2514,11 @@

      Args

      edgeitems=super()._edgeitems, linewidth=super()._linewidth, ) + limit = 50 + header_hex = ' '.join(f'{byte:02X}' for byte in self.header_as_bytes()[:limit]) + data_hex = ' '.join(f'{byte:02X}' for byte in self.data[:limit]) + data_ascii = ''.join((chr(byte) if 32 <= byte <= 126 else '.') for byte in self.data[:limit]) + msg = ( f"{self.__class__.__name__}:\n" f" Logical Address = 0x{self.logical_address:02X}\n" @@ -2794,7 +2527,9 @@

      Args

      f" Type = {self._type}\n" f" Frame Counter = {self.frame_counter}\n" f" Sequence Counter = {self.sequence_counter}\n" - f" Data = \n{self.data}" + f" Header = {header_hex}\n" + f" Data HEX = {data_hex}\n" + f" Data ASC = {data_ascii}\n" ) np.set_printoptions(**options) return msg
      @@ -3531,6 +3266,20 @@

      Instance variables

    +
    +class ExtensionPacket +
    +
    +
    +
    + +Expand source code + +
    class ExtensionPacket:
    +    def __init__(self):
    +        pass
    +
    +
    class HousekeepingPacket (data: Union[bytes, numpy.ndarray]) @@ -3700,77 +3449,302 @@

    Inherited members

    Expand source code -
    class PacketType(IntEnum):
    -    """Enumeration type that defines the SpaceWire packet type."""
    +
    class PacketType(IntEnum):
    +    """Enumeration type that defines the SpaceWire packet type."""
    +
    +    DATA_PACKET = 0
    +    OVERSCAN_DATA = 1
    +    HOUSEKEEPING_DATA = 2  # N-FEE
    +    DEB_HOUSEKEEPING_DATA = 2  # F-FEE
    +    AEB_HOUSEKEEPING_DATA = 3  # F-FEE
    + +

    Ancestors

    +
      +
    • enum.IntEnum
    • +
    • builtins.int
    • +
    • enum.Enum
    • +
    +

    Class variables

    +
    +
    var AEB_HOUSEKEEPING_DATA
    +
    +
    +
    +
    var DATA_PACKET
    +
    +
    +
    +
    var DEB_HOUSEKEEPING_DATA
    +
    +
    +
    +
    var HOUSEKEEPING_DATA
    +
    +
    +
    +
    var OVERSCAN_DATA
    +
    +
    +
    +
    + +
    +class RMAPPacket +(data: Union[bytes, numpy.ndarray]) +
    +
    +

    Base class for RMAP SpaceWire packets.

    +

    Args

    +
    +
    data
    +
    a bytes object or a numpy array of type np.uint8 (not enforced)
    +
    +
    + +Expand source code + +
    class RMAPPacket(SpaceWirePacket):
    +    """Base class for RMAP SpaceWire packets."""
    +
    +    def __init__(self, data: Union[bytes, np.ndarray]):
    +        if not self.is_rmap_packet(data):
    +            raise ValueError(f"Can not create a RMAPPacket from the given data {data}")
    +        super().__init__(data)
    +
    +    def __str__(self):
    +        msg = (
    +            f"RMAP Base Packet ({len(self)} bytes)\n"
    +            f"Logical address:    0x{self.logical_address:02x}\n"
    +            f"Protocol ID:        0x{self.protocol_id:02x}\n"
    +            f"Instruction:        0x{self.instruction:02x} (0o{self.instruction:08b})\n"
    +            f"Transaction ID:     0x{self.transaction_id:04x} ({self.transaction_id})\n"
    +            f"Data = {self._bytes}\n"
    +        )
    +        return msg
    +
    +    @property
    +    def instruction(self):
    +        return get_instruction_field(self._bytes)
    +
    +    @property
    +    def transaction_id(self):
    +        return get_transaction_identifier(self._bytes)
    +
    +    @property
    +    def header_crc(self):
    +        return get_header_crc(self._bytes)
    +
    +    @property
    +    def data_crc(self):
    +        return get_data_crc(self._bytes)
    +
    +    @classmethod
    +    def is_rmap_packet(cls, data: Union[bytes, np.ndarray]):
    +        if data[1] == 0x01:  # Protocol ID
    +            return True
    +        return False
    +
    +

    Ancestors

    + +

    Subclasses

    + +

    Static methods

    +
    +
    +def is_rmap_packet(data: Union[bytes, numpy.ndarray]) +
    +
    +
    +
    + +Expand source code + +
    @classmethod
    +def is_rmap_packet(cls, data: Union[bytes, np.ndarray]):
    +    if data[1] == 0x01:  # Protocol ID
    +        return True
    +    return False
    +
    +
    +
    +

    Instance variables

    +
    +
    var data_crc
    +
    +
    +
    + +Expand source code + +
    @property
    +def data_crc(self):
    +    return get_data_crc(self._bytes)
    +
    +
    +
    var header_crc
    +
    +
    +
    + +Expand source code + +
    @property
    +def header_crc(self):
    +    return get_header_crc(self._bytes)
    +
    +
    +
    var instruction
    +
    +
    +
    + +Expand source code + +
    @property
    +def instruction(self):
    +    return get_instruction_field(self._bytes)
    +
    +
    +
    var transaction_id
    +
    +
    +
    + +Expand source code + +
    @property
    +def transaction_id(self):
    +    return get_transaction_identifier(self._bytes)
    +
    +
    +
    +

    Inherited members

    + +
    +
    +class RMAPRequestMixin +(data: Union[bytes, numpy.ndarray]) +
    +
    +

    Base class for any packet transmitted over a SpaceWire cable.

    +

    Args

    +
    +
    data
    +
    a bytes object or a numpy array of type np.uint8 (not enforced)
    +
    +
    + +Expand source code + +
    class RMAPRequestMixin(SpaceWirePacket):
    +    @property
    +    def key(self):
    +        """Returns the key field."""
    +        return get_key_field(self._bytes)
     
    -    DATA_PACKET = 0
    -    OVERSCAN_DATA = 1
    -    HOUSEKEEPING_DATA = 2  # N-FEE
    -    DEB_HOUSEKEEPING_DATA = 2  # F-FEE
    -    AEB_HOUSEKEEPING_DATA = 3  # F-FEE
    + @property + def initiator_address(self): + """Returns the initiator logical address.""" + return self._bytes[4] + + @property + def data_length(self): + return get_data_length(self._bytes) + + @property + def address(self): + return get_address(self._bytes)

    Ancestors

    -

    Class variables

    +

    Subclasses

    + +

    Instance variables

    -
    var AEB_HOUSEKEEPING_DATA
    -
    -
    -
    -
    var DATA_PACKET
    -
    -
    -
    -
    var DEB_HOUSEKEEPING_DATA
    +
    var address
    +
    + +Expand source code + +
    @property
    +def address(self):
    +    return get_address(self._bytes)
    +
    -
    var HOUSEKEEPING_DATA
    +
    var data_length
    +
    + +Expand source code + +
    @property
    +def data_length(self):
    +    return get_data_length(self._bytes)
    +
    -
    var OVERSCAN_DATA
    +
    var initiator_address
    -
    -
    -
    +

    Returns the initiator logical address.

    +
    + +Expand source code + +
    @property
    +def initiator_address(self):
    +    """Returns the initiator logical address."""
    +    return self._bytes[4]
    +
    -
    -class RMAPError -(*args, **kwargs) -
    +
    var key
    -

    An RMAP specific Error.

    +

    Returns the key field.

    Expand source code -
    class RMAPError(Error):
    -    """An RMAP specific Error."""
    -    pass
    +
    @property
    +def key(self):
    +    """Returns the key field."""
    +    return get_key_field(self._bytes)
    -

    Ancestors

    +
    + +

    Inherited members

    -

    Subclasses

    +
  • SpaceWirePacket: +
  • -
    -class RMAPPacket +
    +class RMAPRequestReplyMixin (data: Union[bytes, numpy.ndarray])
    -

    Base class for RMAP SpaceWire packets.

    +

    Base class for any packet transmitted over a SpaceWire cable.

    Args

    data
    @@ -3780,34 +3754,16 @@

    Args

    Expand source code -
    class RMAPPacket(SpaceWirePacket):
    -    """Base class for RMAP SpaceWire packets."""
    -
    -    def __init__(self, data: Union[bytes, np.ndarray]):
    -        if not self.is_rmap_packet(data):
    -            raise ValueError(f"Can not create a RMAPPacket from the given data {data}")
    -        super().__init__(data)
    -
    -    def __str__(self):
    -        return (
    -            f"{self.__class__.__name__}:\n"
    -            f"  Logical Address = 0x{self.logical_address:02X}\n"
    -            f"  Data = {self.data}\n"
    -        )
    -
    +
    class RMAPRequestReplyMixin(SpaceWirePacket):
         @property
    -    def instruction(self):
    -        return get_instruction_field(self._bytes)
    +    def target_address(self):
    +        """Returns the target logical address."""
    +        return self._bytes[4]
     
         @property
    -    def transaction_id(self):
    -        return get_transaction_identifier(self._bytes)
    -
    -    @classmethod
    -    def is_rmap_packet(cls, data: Union[bytes, np.ndarray]):
    -        if data[1] == 0x01:  # Protocol ID
    -            return True
    -        return False
    + def status(self): + """Returns the status field.""" + return self._bytes[3]

    Ancestors

      @@ -3815,54 +3771,35 @@

      Ancestors

    Subclasses

    -

    Static methods

    -
    -
    -def is_rmap_packet(data: Union[bytes, numpy.ndarray]) -
    -
    -
    -
    - -Expand source code - -
    @classmethod
    -def is_rmap_packet(cls, data: Union[bytes, np.ndarray]):
    -    if data[1] == 0x01:  # Protocol ID
    -        return True
    -    return False
    -
    -
    -

    Instance variables

    -
    var instruction
    +
    var status
    -
    +

    Returns the status field.

    Expand source code
    @property
    -def instruction(self):
    -    return get_instruction_field(self._bytes)
    +def status(self): + """Returns the status field.""" + return self._bytes[3]
    -
    var transaction_id
    +
    var target_address
    -
    +

    Returns the target logical address.

    Expand source code
    @property
    -def transaction_id(self):
    -    return get_transaction_identifier(self._bytes)
    +def target_address(self): + """Returns the target logical address.""" + return self._bytes[4]
    @@ -3890,7 +3827,7 @@

    Args

    Expand source code -
    class ReadRequest(RMAPPacket):
    +
    class ReadRequest(RMAPPacket, RMAPRequestMixin):
         """A Read Request SpaceWire RMAP Packet."""
     
         def __init__(self, data: Union[bytes, np.ndarray]):
    @@ -3906,23 +3843,26 @@ 

    Args

    return True return False - @property - def address(self): - return get_address(self._bytes) - - @property - def data_length(self): - return get_data_length(self._bytes) - def __str__(self): - return ( - f"Read Request: tid={self.transaction_id}, address=0x{self.address:04x}, " - f"data length={self.data_length}" - )
    + msg = ( + f"RMAP Read Request ({len(self)} bytes)\n" + f"Target address: 0x{self.logical_address:02x}\n" + f"Protocol ID: 0x{self.protocol_id:02x}\n" + f"Instruction: 0x{self.instruction:02x} (0o{self.instruction:08b})\n" + f"Key: 0x{self.key:02x}\n" + f"Initiator address: 0x{self.initiator_address:02x}\n" + f"Transaction ID: 0x{self.transaction_id:04x} ({self.transaction_id})\n" + f"(Extended) Address: 0x{self.address:08x}\n" + f"Data Length: {self.data_length}\n" + f"Header CRC: 0x{self.header_crc:02x}\n" + ) + + return msg

    Ancestors

    Static methods

    @@ -3948,33 +3888,6 @@

    Static methods

    -

    Instance variables

    -
    -
    var address
    -
    -
    -
    - -Expand source code - -
    @property
    -def address(self):
    -    return get_address(self._bytes)
    -
    -
    -
    var data_length
    -
    -
    -
    - -Expand source code - -
    @property
    -def data_length(self):
    -    return get_data_length(self._bytes)
    -
    -
    -

    Inherited members

    +
  • RMAPRequestMixin: + +
  • @@ -3999,7 +3918,7 @@

    Args

    Expand source code -
    class ReadRequestReply(RMAPPacket):
    +
    class ReadRequestReply(RMAPPacket, RMAPRequestReplyMixin):
         """An RMAP Reply packet to a Read Request."""
     
         def __init__(self, data: Union[bytes, np.ndarray]):
    @@ -4024,12 +3943,33 @@ 

    Args

    def __str__(self): data_length = self.data_length - return f"Read Request Reply: data length={data_length}, data={self.data[:20]} " \ - f"{'(data is cut to max 20 bytes)' if data_length > 20 else ''}\n"
    + limit = 32 + data_hex = ' '.join(f'{x:02X}' for x in self.data[:limit]) + data_hex += ' ...' if len(self.data) > limit else '' + data_asc = ''.join((chr(byte) if 32 <= byte <= 126 else '.') for byte in self.data[:limit]) + data_asc += ' ...' if len(self.data) > limit else '' + + msg = ( + f"Read Request Reply ({len(self)} bytes)\n" + f"Initiator address: 0x{self.logical_address:02x}\n" + f"Protocol ID: 0x{self.protocol_id:02x}\n" + f"Instruction: 0x{self.instruction:02x} (0o{self.instruction:08b})\n" + f"Status: 0x{self.status:02x}\n" + f"target address: 0x{self.target_address:02x}\n" + f"transaction ID: 0x{self.transaction_id:02x} ({self.transaction_id})\n" + f"Data Length: {self.data_length}\n" + f"Header CRC: 0x{self.header_crc:02x}\n" + f"Data (hex): 0x{data_hex}\n" + f"Data (ascii): {data_asc}\n" + f"Data CRC: 0x{self.data_crc:02x}\n" + ) + + return msg

    Ancestors

    Static methods

    @@ -4088,6 +4028,12 @@

    Inherited members

  • create_packet
  • +
  • RMAPRequestReplyMixin: + +
  • @@ -4108,6 +4054,7 @@

    Inherited members

    def __enter__(self): self.connect() + return self def __exit__(self, exc_type, exc_val, exc_tb): self.disconnect() @@ -4125,6 +4072,7 @@

    Inherited members

    raise NotImplementedError def send_timecode(self, timecode: int): + """Send a timecode over the transport layer.""" raise NotImplementedError def read_packet(self, timeout: int = None) -> Tuple[int, bytes]: @@ -4364,12 +4312,13 @@

    Returns

    def send_timecode(self, timecode: int)
    -
    +

    Send a timecode over the transport layer.

    Expand source code
    def send_timecode(self, timecode: int):
    +    """Send a timecode over the transport layer."""
         raise NotImplementedError
    @@ -4421,7 +4370,7 @@

    Args

    Raises

    -
    RMAPError
    +
    RMAPError
    when data can not be written on the target, i.e. the N-FEE.
    @@ -4483,17 +4432,18 @@

    Args

    self._bytes = bytes(data) def __repr__(self): - options = np.get_printoptions() - np.set_printoptions( - formatter={"int": lambda x: f"0x{x:02x}"}, - threshold=self._threshold, - edgeitems=self._edgeitems, - linewidth=self._linewidth, + limit = 25 + data_hex = ' '.join(f"{x:02x}" for x in self._bytes[:limit]) + data_hex += '...' if len(self._bytes) > limit else '' + + msg = ( + f"{self.__class__.__name__}(0x{data_hex})" ) - msg = f"{self.__class__.__name__}({self._bytes})" - np.set_printoptions(**options) return msg + def __len__(self): + return len(self._bytes) + @property def packet_as_bytes(self): return self._bytes @@ -4522,6 +4472,8 @@

    Args

    Factory method that returns a SpaceWire packet of the correct type based on the information in the header. """ + # MODULE_LOGGER.info(f"{len(data) = }") + if TimecodePacket.is_timecode_packet(data): return TimecodePacket(data) if HousekeepingPacket.is_housekeeping_packet(data): @@ -4544,6 +4496,8 @@

    Subclasses

    Static methods

    @@ -4564,6 +4518,8 @@

    Static methods

    Factory method that returns a SpaceWire packet of the correct type based on the information in the header. """ + # MODULE_LOGGER.info(f"{len(data) = }") + if TimecodePacket.is_timecode_packet(data): return TimecodePacket(data) if HousekeepingPacket.is_housekeeping_packet(data): @@ -4694,7 +4650,7 @@

    Args

    return data[0] == 0x91 def __str__(self): - return f"Timecode Packet: timecode = 0x{self.timecode:x}"
    + return f"Timecode Packet: timecode = 0x{self.timecode:02x} ({self.timecode:2d})"

    Ancestors

      @@ -4772,7 +4728,7 @@

      Args

      Expand source code -
      class WriteRequest(RMAPPacket):
      +
      class WriteRequest(RMAPPacket, RMAPRequestMixin):
           """A Write Request SpaceWire RMAP Packet."""
       
           def __init__(self, data: Union[bytes, np.ndarray]):
      @@ -4784,10 +4740,6 @@ 

      Args

      def is_unverified(self): return self._bytes[2] == 0x6C - @property - def address(self): - return get_address(self._bytes) - @property def data_length(self): return get_data_length(self._bytes) @@ -4808,11 +4760,34 @@

      Args

      def __str__(self): prefix = "Verified" if self.is_verified() else "Unverified" - return f"{prefix} Write Request: {self.transaction_id=}, data=0x{self.data.hex()}"
      + limit = 32 + data_hex = ' '.join(f'{x:02X}' for x in self.data[:limit]) + data_hex += ' ...' if len(self.data) > limit else '' + data_asc = ''.join((chr(byte) if 32 <= byte <= 126 else '.') for byte in self.data[:limit]) + data_asc += ' ...' if len(self.data) > limit else '' + + packet = self.packet_as_bytes + msg = ( + f"RMAP {prefix} Write Request ({len(packet)} bytes)\n" + f"Target address: 0x{self.logical_address:02x}\n" + f"Protocol ID: 0x{self.protocol_id:02x}\n" + f"Instruction: 0x{self.instruction:02x} (0o{self.instruction:08b})\n" + f"Key: 0x{self.key:02x}\n" + f"Initiator address: 0x{self.initiator_address:02x}\n" + f"Transaction ID: 0x{self.transaction_id:04x} ({self.transaction_id})\n" + f"Address: 0x{self.address:08x}\n" + f"Data Length: {self.data_length}\n" + f"Header CRC: 0x{self.header_crc:02x}\n" + f"data (hex): 0x{data_hex}\n" + f"data (ascii): {data_asc}\n" + f"Data CRC: 0x{self.data_crc:02x}\n" + ) + return msg

      Ancestors

      Static methods

      @@ -4840,18 +4815,6 @@

      Static methods

      Instance variables

      -
      var address
      -
      -
      -
      - -Expand source code - -
      @property
      -def address(self):
      -    return get_address(self._bytes)
      -
      -
      var data : bytes
      @@ -4913,6 +4876,12 @@

      Inherited members

    • create_packet
    +
  • RMAPRequestMixin: + +
  • @@ -4930,12 +4899,11 @@

    Args

    Expand source code -
    class WriteRequestReply(RMAPPacket):
    +
    class WriteRequestReply(RMAPPacket, RMAPRequestReplyMixin):
         """An RMAP Reply packet to a Write Request."""
     
         def __init__(self, data: Union[bytes, np.ndarray]):
             super().__init__(data)
    -        self._status = data[3]
     
         @classmethod
         def is_write_reply(cls, data: Union[bytes, np.ndarray]):
    @@ -4946,16 +4914,24 @@ 

    Args

    if (data[2] == 0x3C or data[2] == 0x2C) and data[4] == 0x51: return True - @property - def status(self): - return self._status - def __str__(self): - return f"Write Request Reply: status={self.status}"
    + msg = ( + f"Write Request Reply ({len(self)} bytes)\n" + f"Initiator address: 0x{self.logical_address:02x}\n" + f"Protocol ID: 0x{self.protocol_id:02x}\n" + f"Instruction: 0x{self.instruction:02x} (0o{self.instruction:08b})\n" + f"Status: 0x{self.status:02x}\n" + f"target address: 0x{self.target_address:02x}\n" + f"transaction ID: 0x{self.transaction_id:02x} ({self.transaction_id})\n" + f"Header CRC: 0x{self.header_crc:02x}\n" + ) + + return msg

    Ancestors

    Static methods

    @@ -4980,21 +4956,6 @@

    Static methods

    -

    Instance variables

    -
    -
    var status
    -
    -
    -
    - -Expand source code - -
    @property
    -def status(self):
    -    return self._status
    -
    -
    -

    Inherited members

    +
  • RMAPRequestReplyMixin: + +
  • @@ -5020,20 +4987,24 @@

    Index

  • Functions

    @@ -5104,6 +5075,9 @@

    Dat

  • +

    ExtensionPacket

    +
  • +
  • HousekeepingPacket

  • -

    RMAPError

    -
  • -
  • RMAPPacket

  • +

    RMAPRequestMixin

    + +
  • +
  • +

    RMAPRequestReplyMixin

    + +
  • +
  • ReadRequest

  • @@ -5188,8 +5175,7 @@

    Tim
  • WriteRequest

    -
      -
    • address
    • + diff --git a/docs/api/egse/stages/aerotech/ensemble.html b/docs/api/egse/stages/aerotech/ensemble.html index cea7ce4..2494e3b 100644 --- a/docs/api/egse/stages/aerotech/ensemble.html +++ b/docs/api/egse/stages/aerotech/ensemble.html @@ -26,22 +26,20 @@

      Module egse.stages.aerotech.ensemble

      Expand source code -
      import enum
      -import logging
      -import time
      +
      import logging
       
       from prometheus_client import Gauge
       
      -from egse.control import ControlServer
       from egse.command import ClientServerCommand
      +from egse.control import ControlServer
       from egse.protocol import CommandProtocol
       from egse.proxy import Proxy
       from egse.settings import Settings
      -from egse.zmq_ser import bind_address
      -from egse.system import format_datetime
      -from egse.stages.aerotech.ensemble_interface import EnsembleInterface
       from egse.stages.aerotech.ensemble_controller import EnsembleController
      +from egse.stages.aerotech.ensemble_interface import EnsembleInterface
       from egse.stages.aerotech.ensemble_simulator import EnsembleSimulator
      +from egse.system import format_datetime
      +from egse.zmq_ser import bind_address
       from egse.zmq_ser import connect_address
       
       PLANESTATUS = {
      diff --git a/docs/api/egse/stages/aerotech/ensemble_controller.html b/docs/api/egse/stages/aerotech/ensemble_controller.html
      index 6991b56..2585741 100644
      --- a/docs/api/egse/stages/aerotech/ensemble_controller.html
      +++ b/docs/api/egse/stages/aerotech/ensemble_controller.html
      @@ -32,10 +32,10 @@ 

      Module egse.stages.aerotech.ensemble_controllerModule egse.stages.aerotech.ensemble_controllerClasses

      self._commands[name] = Command(name, items['cmd']) # Get the movement speed from the setup - setup = GlobalState.setup + setup = load_setup() self.speed = setup.gse.ensemble.maximum_speed self.limit = setup.gse.ensemble.limit diff --git a/docs/api/egse/stages/aerotech/ensemble_cs.html b/docs/api/egse/stages/aerotech/ensemble_cs.html index a609420..2684a09 100644 --- a/docs/api/egse/stages/aerotech/ensemble_cs.html +++ b/docs/api/egse/stages/aerotech/ensemble_cs.html @@ -299,10 +299,15 @@

      Inherited members

      • ControlServer:
      • diff --git a/docs/api/egse/stages/aerotech/mgse_sim.html b/docs/api/egse/stages/aerotech/mgse_sim.html index d22accc..ea6c457 100644 --- a/docs/api/egse/stages/aerotech/mgse_sim.html +++ b/docs/api/egse/stages/aerotech/mgse_sim.html @@ -28,15 +28,10 @@

        Module egse.stages.aerotech.mgse_sim

        import multiprocessing.process
         
        -from sympy import re
         multiprocessing.current_process().name = "mgse_sim"
         
         import logging
        -import random
        -import re
         import socket
        -import time
        -from enum import Enum
         from egse.settings import Settings
         
         import click
        diff --git a/docs/api/egse/stages/arun/smd3.html b/docs/api/egse/stages/arun/smd3.html
        index 6817833..19733b5 100644
        --- a/docs/api/egse/stages/arun/smd3.html
        +++ b/docs/api/egse/stages/arun/smd3.html
        @@ -30,17 +30,16 @@ 

        Module egse.stages.arun.smd3

        from prometheus_client import Gauge -from egse.control import ControlServer from egse.command import ClientServerCommand +from egse.control import ControlServer from egse.protocol import CommandProtocol -# from egse.fdir.fdir_manager import FdirManagerProxy from egse.proxy import Proxy from egse.settings import Settings -from egse.zmq_ser import bind_address -from egse.system import format_datetime -from egse.stages.arun.smd3_interface import Smd3Interface from egse.stages.arun.smd3_controller import Smd3Controller, EFLAGS, SFLAGS +from egse.stages.arun.smd3_interface import Smd3Interface from egse.stages.arun.smd3_simulator import Smd3Simulator +from egse.system import format_datetime +from egse.zmq_ser import bind_address from egse.zmq_ser import connect_address logger = logging.getLogger(__name__) diff --git a/docs/api/egse/stages/arun/smd3_controller.html b/docs/api/egse/stages/arun/smd3_controller.html index 15fd504..1e859e7 100644 --- a/docs/api/egse/stages/arun/smd3_controller.html +++ b/docs/api/egse/stages/arun/smd3_controller.html @@ -27,17 +27,15 @@

        Module egse.stages.arun.smd3_controller

        Expand source code
        import logging
        -import time
        -import os
         from threading import Thread
        -import threading 
        +
         import rich
        +import time
         
         from egse.serialdevice import SerialDevice
        -from egse.command import Command
         from egse.settings import Settings
        +from egse.setup import load_setup
         from egse.stages.arun.smd3_interface import Smd3Interface
        -from egse.state import GlobalState
         
         logger = logging.getLogger('smd3_controller')
         
        @@ -89,7 +87,7 @@ 

        Module egse.stages.arun.smd3_controller

        self._port = DEVICE_SETTINGS.PORT if port is None else port self._baudrate = DEVICE_SETTINGS.BAUDRATE if baudrate is None else baudrate - setup = GlobalState.setup + setup = load_setup() self._in_fov_position = setup.gse.smd3.configuration.in_fov_position self.status = {} @@ -347,11 +345,11 @@

        Module egse.stages.arun.smd3_controller

        def configure_homing(self): self.select_mode(Smd3Mode.HOME) - self.target_frequency(100) + self.target_frequency(500) def configure_remote(self): self.select_mode(Smd3Mode.REMOTE) - self.target_frequency(500) + self.target_frequency(1000) def save_status(self, status_word): for bits, name in SFLAGS.items(): @@ -399,15 +397,10 @@

        Module egse.stages.arun.smd3_controller

        # Home negative side self.home('-') - # Wait until we stat moving - time.sleep(0.1) - _, _, velocity = self.actual_frequency() - while float(velocity) != 0.0: - _, _, velocity = self.actual_frequency() - time.sleep(1) + while not int(self.actual_position()[0], 16) >> 1 & 0x1: + time.sleep(0.5) - # Wait a little bit until we actually stopped - time.sleep(0.1) + time.sleep(.5) # Reset actual position self.actual_position(0.0) @@ -569,7 +562,7 @@

        Classes

        self._port = DEVICE_SETTINGS.PORT if port is None else port self._baudrate = DEVICE_SETTINGS.BAUDRATE if baudrate is None else baudrate - setup = GlobalState.setup + setup = load_setup() self._in_fov_position = setup.gse.smd3.configuration.in_fov_position self.status = {} @@ -827,11 +820,11 @@

        Classes

        def configure_homing(self): self.select_mode(Smd3Mode.HOME) - self.target_frequency(100) + self.target_frequency(500) def configure_remote(self): self.select_mode(Smd3Mode.REMOTE) - self.target_frequency(500) + self.target_frequency(1000) def save_status(self, status_word): for bits, name in SFLAGS.items(): @@ -879,15 +872,10 @@

        Classes

        # Home negative side self.home('-') - # Wait until we stat moving - time.sleep(0.1) - _, _, velocity = self.actual_frequency() - while float(velocity) != 0.0: - _, _, velocity = self.actual_frequency() - time.sleep(1) + while not int(self.actual_position()[0], 16) >> 1 & 0x1: + time.sleep(0.5) - # Wait a little bit until we actually stopped - time.sleep(0.1) + time.sleep(.5) # Reset actual position self.actual_position(0.0) @@ -1111,7 +1099,7 @@

        Methods

        def configure_homing(self):
             self.select_mode(Smd3Mode.HOME)
        -    self.target_frequency(100)
        + self.target_frequency(500)
        @@ -1144,7 +1132,7 @@

        Methods

        def configure_remote(self):
             self.select_mode(Smd3Mode.REMOTE)
        -    self.target_frequency(500)
        + self.target_frequency(1000)
        @@ -1265,15 +1253,10 @@

        Methods

        # Home negative side self.home('-') - # Wait until we stat moving - time.sleep(0.1) - _, _, velocity = self.actual_frequency() - while float(velocity) != 0.0: - _, _, velocity = self.actual_frequency() - time.sleep(1) + while not int(self.actual_position()[0], 16) >> 1 & 0x1: + time.sleep(0.5) - # Wait a little bit until we actually stopped - time.sleep(0.1) + time.sleep(.5) # Reset actual position self.actual_position(0.0) diff --git a/docs/api/egse/stages/arun/smd3_cs.html b/docs/api/egse/stages/arun/smd3_cs.html index be73803..71fcf6e 100644 --- a/docs/api/egse/stages/arun/smd3_cs.html +++ b/docs/api/egse/stages/arun/smd3_cs.html @@ -302,10 +302,15 @@

        Inherited members

        • ControlServer:
        • diff --git a/docs/api/egse/stages/huber/smc9300.html b/docs/api/egse/stages/huber/smc9300.html index aa0f106..5b8b7a1 100644 --- a/docs/api/egse/stages/huber/smc9300.html +++ b/docs/api/egse/stages/huber/smc9300.html @@ -48,9 +48,9 @@

          Module egse.stages.huber.smc9300

          from egse.mixin import dynamic_command from egse.proxy import DynamicProxy from egse.settings import Settings +from egse.setup import load_setup from egse.stages.huber.smc9300_devif import HuberError from egse.stages.huber.smc9300_devif import HuberSMC9300EthernetInterface -from egse.state import GlobalState from egse.zmq_ser import connect_address # Explicitly set the module name instead of __name__. When module is executed instead of imported @@ -224,6 +224,14 @@

          Module egse.stages.huber.smc9300

          Interface definition for the Controller, Simulator and Proxy classes for this device. """ + def __init__(self): + + super().__init__() + + setup = load_setup() + self.avoidance = setup.gse.stages.big_rotation_stage.avoidance + self.hardstop = setup.gse.stages.big_rotation_stage.hardstop + @dynamic_command( cmd_type="query", cmd_string="?", process_cmd_string=process_cmd_string, @@ -294,7 +302,7 @@

          Module egse.stages.huber.smc9300

          if axis == HC_SETTINGS.BIG_ROTATION_STAGE: current = self.get_current_position(axis) - movement = calculate_relative_movement(current, position) + movement = calculate_relative_movement(current, position, avoidance=self.avoidance, hardstop=self.hardstop) self.move_direct(axis, movement) else: self.goto_direct(axis, position) @@ -799,7 +807,9 @@

          Module egse.stages.huber.smc9300

          [default is taken from settings file] timeout: time out on the response from the control server [milliseconds] """ - super().__init__(connect_address(protocol, hostname, port), timeout=timeout) + + DynamicProxy.__init__(self, connect_address(protocol, hostname, port), timeout=timeout) # explicit calls without super + HuberSMC9300Interface.__init__(self) def calculate_relative_movement(current, setpoint, hardstop=None, avoidance=None): @@ -822,6 +832,7 @@

          Module egse.stages.huber.smc9300

          setpoint (float): the target position of the rotation stage hardstop (float): the location of the hardstop avoidance (float): angle of avoidance + setup: Setup Returns: The angle to move relative from current position to setpoint. Raises: @@ -829,10 +840,14 @@

          Module egse.stages.huber.smc9300

          [-180 + avoidance, +180 - avoidance]. """ - setup = GlobalState.setup + if not avoidance or not hardstop: + setup = load_setup() + + avoidance = avoidance or setup.gse.stages.big_rotation_stage.avoidance + hardstop = hardstop or setup.gse.stages.big_rotation_stage.hardstop - avoidance = avoidance if avoidance is not None else setup.gse.stages.big_rotation_stage.avoidance - hardstop = hardstop if hardstop is not None else setup.gse.stages.big_rotation_stage.hardstop + # avoidance = avoidance if avoidance is not None else setup.gse.stages.big_rotation_stage.avoidance + # hardstop = hardstop if hardstop is not None else setup.gse.stages.big_rotation_stage.hardstop # MODULE_LOGGER.debug(f"@entry: {current=}, {setpoint=}, {hardstop=}, {avoidance=}") @@ -846,6 +861,7 @@

          Module egse.stages.huber.smc9300

          # Check if the setpoint falls in the avoidance range if hardstop - avoidance < setpoint < hardstop + avoidance: + print(f"{hardstop - avoidance} < {setpoint} < {hardstop + avoidance}") raise ValueError( f"setpoint argument shall NOT be between {hardstop - avoidance} and " f"{hardstop + avoidance}, given value {setpoint = }") @@ -963,6 +979,8 @@

          Args

          the location of the hardstop
          avoidance : float
          angle of avoidance
          +
          setup
          +
          Setup

          Returns

          The angle to move relative from current position to setpoint.

          @@ -993,6 +1011,7 @@

          Raises

          setpoint (float): the target position of the rotation stage hardstop (float): the location of the hardstop avoidance (float): angle of avoidance + setup: Setup Returns: The angle to move relative from current position to setpoint. Raises: @@ -1000,10 +1019,14 @@

          Raises

          [-180 + avoidance, +180 - avoidance]. """ - setup = GlobalState.setup + if not avoidance or not hardstop: + setup = load_setup() - avoidance = avoidance if avoidance is not None else setup.gse.stages.big_rotation_stage.avoidance - hardstop = hardstop if hardstop is not None else setup.gse.stages.big_rotation_stage.hardstop + avoidance = avoidance or setup.gse.stages.big_rotation_stage.avoidance + hardstop = hardstop or setup.gse.stages.big_rotation_stage.hardstop + + # avoidance = avoidance if avoidance is not None else setup.gse.stages.big_rotation_stage.avoidance + # hardstop = hardstop if hardstop is not None else setup.gse.stages.big_rotation_stage.hardstop # MODULE_LOGGER.debug(f"@entry: {current=}, {setpoint=}, {hardstop=}, {avoidance=}") @@ -1017,6 +1040,7 @@

          Raises

          # Check if the setpoint falls in the avoidance range if hardstop - avoidance < setpoint < hardstop + avoidance: + print(f"{hardstop - avoidance} < {setpoint} < {hardstop + avoidance}") raise ValueError( f"setpoint argument shall NOT be between {hardstop - avoidance} and " f"{hardstop + avoidance}, given value {setpoint = }") @@ -1490,6 +1514,14 @@

          Inherited members

          Interface definition for the Controller, Simulator and Proxy classes for this device. """ + def __init__(self): + + super().__init__() + + setup = load_setup() + self.avoidance = setup.gse.stages.big_rotation_stage.avoidance + self.hardstop = setup.gse.stages.big_rotation_stage.hardstop + @dynamic_command( cmd_type="query", cmd_string="?", process_cmd_string=process_cmd_string, @@ -1560,7 +1592,7 @@

          Inherited members

          if axis == HC_SETTINGS.BIG_ROTATION_STAGE: current = self.get_current_position(axis) - movement = calculate_relative_movement(current, position) + movement = calculate_relative_movement(current, position, avoidance=self.avoidance, hardstop=self.hardstop) self.move_direct(axis, movement) else: self.goto_direct(axis, position) @@ -2333,7 +2365,7 @@

          Returns

          if axis == HC_SETTINGS.BIG_ROTATION_STAGE: current = self.get_current_position(axis) - movement = calculate_relative_movement(current, position) + movement = calculate_relative_movement(current, position, avoidance=self.avoidance, hardstop=self.hardstop) self.move_direct(axis, movement) else: self.goto_direct(axis, position) @@ -2910,7 +2942,9 @@

          Args

          [default is taken from settings file] timeout: time out on the response from the control server [milliseconds] """ - super().__init__(connect_address(protocol, hostname, port), timeout=timeout)
      + + DynamicProxy.__init__(self, connect_address(protocol, hostname, port), timeout=timeout) # explicit calls without super + HuberSMC9300Interface.__init__(self)

      Ancestors

        diff --git a/docs/api/egse/stages/huber/smc9300_cs.html b/docs/api/egse/stages/huber/smc9300_cs.html index 3809a32..4dba69d 100644 --- a/docs/api/egse/stages/huber/smc9300_cs.html +++ b/docs/api/egse/stages/huber/smc9300_cs.html @@ -389,10 +389,15 @@

        Inherited members

        • ControlServer:
        • diff --git a/docs/api/egse/stages/huber/smc9300_protocol.html b/docs/api/egse/stages/huber/smc9300_protocol.html index bd343e3..2501544 100644 --- a/docs/api/egse/stages/huber/smc9300_protocol.html +++ b/docs/api/egse/stages/huber/smc9300_protocol.html @@ -38,6 +38,7 @@

          Module egse.stages.huber.smc9300_protocol

          from egse.process import SubProcess from egse.protocol import DynamicCommandProtocol from egse.settings import Settings +from egse.setup import load_setup from egse.stages.huber.smc9300 import HuberSMC9300Controller from egse.stages.huber.smc9300 import HuberSMC9300Interface from egse.system import format_datetime @@ -55,10 +56,11 @@

          Module egse.stages.huber.smc9300_protocol

          self.huber_sim: SubProcess self.huber = HuberSMC9300Interface + setup = load_setup() storage_mnemonic = self.get_control_server().get_storage_mnemonic() - self.hk_conversion_table = read_conversion_dict(storage_mnemonic, use_site=True) + self.hk_conversion_table = read_conversion_dict(storage_mnemonic, use_site=True, setup=setup) if Settings.simulation_mode(): # In simulation mode, we start the simulator process and have the control server @@ -81,7 +83,7 @@

          Module egse.stages.huber.smc9300_protocol

          "Couldn't establish a connection to the HUBER Stages, check the log messages.") MODULE_LOGGER.debug(f"{exc = }") - self.metrics = define_metrics(storage_mnemonic, use_site=True) + self.metrics = define_metrics(storage_mnemonic, use_site=True, setup=setup) def quit(self): if self.huber_sim is not None: @@ -166,10 +168,11 @@

          Classes

          self.huber_sim: SubProcess self.huber = HuberSMC9300Interface + setup = load_setup() storage_mnemonic = self.get_control_server().get_storage_mnemonic() - self.hk_conversion_table = read_conversion_dict(storage_mnemonic, use_site=True) + self.hk_conversion_table = read_conversion_dict(storage_mnemonic, use_site=True, setup=setup) if Settings.simulation_mode(): # In simulation mode, we start the simulator process and have the control server @@ -192,7 +195,7 @@

          Classes

          "Couldn't establish a connection to the HUBER Stages, check the log messages.") MODULE_LOGGER.debug(f"{exc = }") - self.metrics = define_metrics(storage_mnemonic, use_site=True) + self.metrics = define_metrics(storage_mnemonic, use_site=True, setup=setup) def quit(self): if self.huber_sim is not None: diff --git a/docs/api/egse/stages/huber/smc9300_ui.html b/docs/api/egse/stages/huber/smc9300_ui.html index 920c5d6..645b8fa 100644 --- a/docs/api/egse/stages/huber/smc9300_ui.html +++ b/docs/api/egse/stages/huber/smc9300_ui.html @@ -1159,7 +1159,7 @@

          Ancestors

          (exc: Exception, parent=None)
  • -

    QDialog(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QDialog(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -2051,7 +2051,7 @@

    Methods

    class HuberUIView
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -2625,7 +2625,7 @@

    Methods

    (axis: int, speed: int, parent=None)
    -

    QDialog(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QDialog(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code @@ -3138,7 +3138,7 @@

    Methods

    (connect: Callable = None, parent: Optional[PyQt5.QtWidgets.QWidget] = None)
    -

    QFrame(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QFrame(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code diff --git a/docs/api/egse/storage/index.html b/docs/api/egse/storage/index.html index ec51861..d1e14ea 100644 --- a/docs/api/egse/storage/index.html +++ b/docs/api/egse/storage/index.html @@ -220,6 +220,7 @@

    How are the files named?

    import logging import os import shutil +from functools import partial from pathlib import Path from pathlib import PurePath from typing import Dict @@ -237,11 +238,15 @@

    How are the files named?

    from egse.decorators import dynamic_interface from egse.env import get_data_storage_location from egse.exceptions import Error +from egse.listener import Event +from egse.listener import EventInterface from egse.obsid import ObservationIdentifier from egse.obsid import TEST_LAB from egse.protocol import CommandProtocol from egse.proxy import Proxy from egse.settings import Settings +from egse.setup import Setup +from egse.setup import get_setup from egse.storage.persistence import HDF5 from egse.storage.persistence import PersistenceLayer from egse.system import format_datetime @@ -565,6 +570,21 @@

    How are the files named?

    pass + @dynamic_interface + def get_loaded_setup_id(self) -> str: + """ + Returns the ID of the currently loaded Setup. + + Note: + This is the Setup active on this control server. This command is mainly used to check that the Setup + loaded in this control server corresponds to the Setup loaded in the configuration manager. + + Returns: + The ID of the Setup loaded in this control server. + """ + + pass + def _disentangle_filename(filename: Union[str, Path]) -> Tuple: """Disentangle the given filename and return the test identifier, the site id and the Setup id. @@ -757,16 +777,18 @@

    How are the files named?

    return 1 -class StorageController(StorageInterface): +class StorageController(StorageInterface, EventInterface): """ The Storage Controller handles the registration of components, the start and end of an observation/test and the dispatching of the persistence functions in save. """ - def __init__(self): + def __init__(self, control_server): self._obsid: ObservationIdentifier | None = None self._camera_name: str | None = None self._registry = Registry() + self._cs: ControlServer = control_server + self._setup: Setup | None = None def start_observation(self, obsid: ObservationIdentifier, camera_name: str = None) -> Response: if self._obsid is not None: @@ -776,6 +798,11 @@

    How are the files named?

    self._obsid = obsid self._camera_name = camera_name + if camera_name != self._setup.camera.ID.lower(): + logger.error( + f"Mismatch in camera name between Setup in Storage Manager {self._setup.camera.ID.lower()} " + f"and Setup in Configuration Manager {camera_name}!" + ) # open a dedicated file for each registered item @@ -1072,12 +1099,48 @@

    How are the files named?

    total, used, free = shutil.disk_usage(location) return total, used, free + def get_loaded_setup_id(self) -> str: + return self._setup.get_id() if self._setup is not None else "no setup loaded" + + def load_setup(self, setup_id: int = 0): + # Use get_setup() here instead of load_setup() in order to prevent recursively notifying and loading Setups. + # That is because the load_setup() method will notify the listeners that a new Setup has been loaded. + try: + setup = get_setup() + except Exception as exc: + raise RuntimeError(f"Exception caught: {exc!r}") + + if setup is None: + raise RuntimeError("Couldn't get Setup from the configuration manager.") + + if isinstance(setup, Failure): + raise setup + + # time.sleep(20.0) # used as a test to check if this method is blocking the commanding... it is! + + # logger.info(f"{setup_id = }, {setup.get_id() = }") + + if 0 < setup_id != int(setup.get_id()): + raise RuntimeError(f"Setup IDs do not match: {setup.get_id()} != {setup_id}, no Setup loaded.") + else: + self._setup = setup + logger.info(f"Setup {setup.get_id()} loaded in the Storage manager.") + + def handle_event(self, event: Event) -> str: + logger.info(f"An event is received, {event=}") + try: + if event.type == 'new_setup': + self._cs.schedule_task(partial(self.load_setup, setup_id=event.context['setup_id'])) + except KeyError as exc: + return f"Expected event context to contain the following key: {exc}" + return "ACK" + class StorageCommand(ClientServerCommand): pass -class StorageProxy(Proxy, StorageInterface): +class StorageProxy(Proxy, StorageInterface, EventInterface): """The StorageProxy class is used to connect to the Storage Manager (control server) and send commands remotely.""" @@ -1103,7 +1166,7 @@

    How are the files named?

    super().__init__() self.control_server = control_server - self.controller = StorageController() + self.controller = StorageController(control_server) self.load_commands(COMMAND_SETTINGS.Commands, StorageCommand, StorageController) @@ -1563,6 +1626,7 @@

    Inherited members

    class StorageController +(control_server)

    The Storage Controller handles the registration of components, the start and end of an @@ -1571,16 +1635,18 @@

    Inherited members

    Expand source code -
    class StorageController(StorageInterface):
    +
    class StorageController(StorageInterface, EventInterface):
         """
         The Storage Controller handles the registration of components, the start and end of an
         observation/test and the dispatching of the persistence functions in save.
         """
     
    -    def __init__(self):
    +    def __init__(self, control_server):
             self._obsid: ObservationIdentifier | None = None
             self._camera_name: str | None = None
             self._registry = Registry()
    +        self._cs: ControlServer = control_server
    +        self._setup: Setup | None = None
     
         def start_observation(self, obsid: ObservationIdentifier, camera_name: str = None) -> Response:
             if self._obsid is not None:
    @@ -1590,6 +1656,11 @@ 

    Inherited members

    self._obsid = obsid self._camera_name = camera_name + if camera_name != self._setup.camera.ID.lower(): + logger.error( + f"Mismatch in camera name between Setup in Storage Manager {self._setup.camera.ID.lower()} " + f"and Setup in Configuration Manager {camera_name}!" + ) # open a dedicated file for each registered item @@ -1884,11 +1955,48 @@

    Inherited members

    location = Path(get_data_storage_location(site_id=SITE.ID)) total, used, free = shutil.disk_usage(location) - return total, used, free
    + return total, used, free + + def get_loaded_setup_id(self) -> str: + return self._setup.get_id() if self._setup is not None else "no setup loaded" + + def load_setup(self, setup_id: int = 0): + # Use get_setup() here instead of load_setup() in order to prevent recursively notifying and loading Setups. + # That is because the load_setup() method will notify the listeners that a new Setup has been loaded. + try: + setup = get_setup() + except Exception as exc: + raise RuntimeError(f"Exception caught: {exc!r}") + + if setup is None: + raise RuntimeError("Couldn't get Setup from the configuration manager.") + + if isinstance(setup, Failure): + raise setup + + # time.sleep(20.0) # used as a test to check if this method is blocking the commanding... it is! + + # logger.info(f"{setup_id = }, {setup.get_id() = }") + + if 0 < setup_id != int(setup.get_id()): + raise RuntimeError(f"Setup IDs do not match: {setup.get_id()} != {setup_id}, no Setup loaded.") + else: + self._setup = setup + logger.info(f"Setup {setup.get_id()} loaded in the Storage manager.") + + def handle_event(self, event: Event) -> str: + logger.info(f"An event is received, {event=}") + try: + if event.type == 'new_setup': + self._cs.schedule_task(partial(self.load_setup, setup_id=event.context['setup_id'])) + except KeyError as exc: + return f"Expected event context to contain the following key: {exc}" + return "ACK"

    Ancestors

    Methods

    @@ -1960,6 +2068,40 @@

    Methods

    return get_data_storage_location(site_id=SITE.ID)
    +
    +def load_setup(self, setup_id: int = 0) +
    +
    +
    +
    + +Expand source code + +
    def load_setup(self, setup_id: int = 0):
    +    # Use get_setup() here instead of load_setup() in order to prevent recursively notifying and loading Setups.
    +    # That is because the load_setup() method will notify the listeners that a new Setup has been loaded.
    +    try:
    +        setup = get_setup()
    +    except Exception as exc:
    +        raise RuntimeError(f"Exception caught: {exc!r}")
    +
    +    if setup is None:
    +        raise RuntimeError("Couldn't get Setup from the configuration manager.")
    +
    +    if isinstance(setup, Failure):
    +        raise setup
    +
    +    # time.sleep(20.0)  # used as a test to check if this method is blocking the commanding... it is!
    +
    +    # logger.info(f"{setup_id = }, {setup.get_id() = }")
    +
    +    if 0 < setup_id != int(setup.get_id()):
    +        raise RuntimeError(f"Setup IDs do not match: {setup.get_id()} != {setup_id}, no Setup loaded.")
    +    else:
    +        self._setup = setup
    +        logger.info(f"Setup {setup.get_id()} loaded in the Storage manager.")
    +
    +
    def save(self, item: dict) ‑> Response
    @@ -2018,6 +2160,7 @@

    Inherited members

  • end_observation
  • get_disk_usage
  • get_filenames
  • +
  • get_loaded_setup_id
  • get_obsid
  • get_registry_names
  • new_registration
  • @@ -2027,6 +2170,11 @@

    Inherited members

  • unregister
  • +
  • EventInterface: + +
  • @@ -2190,6 +2338,21 @@

    Inherited members

    - Free disk space [bytes]. """ + pass + + @dynamic_interface + def get_loaded_setup_id(self) -> str: + """ + Returns the ID of the currently loaded Setup. + + Note: + This is the Setup active on this control server. This command is mainly used to check that the Setup + loaded in this control server corresponds to the Setup loaded in the configuration manager. + + Returns: + The ID of the Setup loaded in this control server. + """ + pass

    Subclasses

    @@ -2302,6 +2465,36 @@

    Returns

    pass +
    +def get_loaded_setup_id(self) ‑> str +
    +
    +

    Returns the ID of the currently loaded Setup.

    +

    Note

    +

    This is the Setup active on this control server. This command is mainly used to check that the Setup +loaded in this control server corresponds to the Setup loaded in the configuration manager.

    +

    Returns

    +

    The ID of the Setup loaded in this control server.

    +
    + +Expand source code + +
    @dynamic_interface
    +def get_loaded_setup_id(self) -> str:
    +    """
    +    Returns the ID of the currently loaded Setup.
    +
    +    Note:
    +        This is the Setup active on this control server. This command is mainly used to check that the Setup
    +        loaded in this control server corresponds to the Setup loaded in the configuration manager.
    +
    +    Returns:
    +        The ID of the Setup loaded in this control server.
    +    """
    +
    +    pass
    +
    +
    def get_obsid(self)
    @@ -2678,7 +2871,7 @@

    Instance variables

    super().__init__() self.control_server = control_server - self.controller = StorageController() + self.controller = StorageController(control_server) self.load_commands(COMMAND_SETTINGS.Commands, StorageCommand, StorageController) @@ -2747,7 +2940,7 @@

    Args

    Expand source code -
    class StorageProxy(Proxy, StorageInterface):
    +
    class StorageProxy(Proxy, StorageInterface, EventInterface):
         """The StorageProxy class is used to connect to the Storage Manager (control server) and
         send commands remotely."""
     
    @@ -2773,6 +2966,7 @@ 

    Ancestors

  • BaseProxy
  • ControlServerConnectionInterface
  • StorageInterface
  • +
  • EventInterface
  • Inherited members

    +
  • EventInterface: + +
  • @@ -2869,6 +3069,7 @@

  • cycle_daily_files
  • get_storage_location
  • +
  • load_setup
  • save
  • @@ -2879,6 +3080,7 @@

    end_observation
  • get_disk_usage
  • get_filenames
  • +
  • get_loaded_setup_id
  • get_obsid
  • get_registry_names
  • get_storage_location
  • diff --git a/docs/api/egse/storage/persistence.html b/docs/api/egse/storage/persistence.html index 5b094a8..d836c3f 100644 --- a/docs/api/egse/storage/persistence.html +++ b/docs/api/egse/storage/persistence.html @@ -32,11 +32,15 @@

    Module egse.storage.persistence

    """ import csv import logging +import os import re import sqlite3 import warnings from abc import ABC from abc import abstractmethod +from math import cos +from math import radians +from math import sin from pathlib import Path from sqlite3 import Connection from typing import Optional @@ -47,13 +51,12 @@

    Module egse.storage.persistence

    import numpy as np from astropy.io import fits from astropy.io.ascii.cparser import AstropyWarning -from math import cos -from math import radians -from math import sin from egse.fee import n_fee_mode +from egse.fee.nfee import HousekeepingData from egse.settings import Settings -from egse.setup import Setup, SetupError +from egse.setup import Setup +from egse.setup import SetupError from egse.spw import DataDataPacket from egse.spw import DataPacket from egse.spw import DataPacketType @@ -62,12 +65,21 @@

    Module egse.storage.persistence

    from egse.spw import PacketType from egse.spw import TimecodePacket from egse.state import GlobalState -from egse.system import read_last_line, time_since_epoch_1958 +from egse.system import read_last_line +from egse.system import time_since_epoch_1958 logger = logging.getLogger(__name__) FOV_SETTINGS = Settings.load("Field-Of-View") CCD_SETTINGS = Settings.load("CCD") +INT_SYNC_TIMING_OFFSET = 0.4 # See https://github.com/IvS-KULeuven/plato-common-egse/issues/2475 + +try: + _ = os.environ["PLATO_CAMERA_IS_EM"] + PLATO_CAMERA_IS_EM = True if _.capitalize() in ("1", "True", "Yes") else 0 +except KeyError: + PLATO_CAMERA_IS_EM = False + class PersistenceLayer(ABC): @@ -226,7 +238,6 @@

    Module egse.storage.persistence

    # Define the values of the WCS keywords - self.site_name = Settings.load().SITE["ID"] self.v_start = prep["v_start"] # First transmitted row self.v_end = prep["v_end"] # Last transmitted row @@ -237,11 +248,12 @@

    Module egse.storage.persistence

    self.obsid = prep["obsid"] self.register_map = prep["register_map"] # Register map - + # Read information from the setup self.setup: Setup = prep["setup"] + self.site_name = self.setup["site_id"] # Site ID self.setup_id = self.setup.get_id() # Setup ID self.camera_id = self.setup.camera.get("ID") # Camera ID (None if not present in the setup) @@ -299,7 +311,10 @@

    Module egse.storage.persistence

    return ((vgd_20 << 4) + vgd_19) / 1000 * 5.983 def get_ccd2_vrd(self): - """ Extract the VRD voltage for CCD2 from the register map. + """ + Extract the VRD voltage for CCD2 from the register map. + + NOTE: Use this function only for the FM cameras. Return: Configured VRD voltage. """ @@ -309,6 +324,20 @@

    Module egse.storage.persistence

    return int(f'0x{vrd_19:x}{vrd_18:x}', 16) + def get_ccd3_vrd(self): + """ + Extract the VRD voltage for CCD3 from the register map. + + NOTE: Use this function only for the EM camera. + + Return: Configured VRD voltage. + """ + vrd_18 = self.register_map[('reg_18_config', 'ccd3_vrd_config')] + vrd_19 = self.register_map[('reg_19_config', 'ccd3_vrd_config')] + + return int(f'0x{vrd_19:x}{vrd_18:x}', 16) + + def init_data_arrays(self): """ Initialise data arrays in which the data content of the SpW packets will be dumped. @@ -547,8 +576,13 @@

    Module egse.storage.persistence

    primary_header["CCD_VOD"] = (self.register_map["ccd_vod_config"], "Configured VOD") primary_header["CCD1_VRD"] = (self.register_map["ccd1_vrd_config"], "Configured VRD for CCD1") - primary_header["CCD2_VRD"] = (self.get_ccd2_vrd(), "Configured VRD for CCD2") - primary_header["CCD3_VRD"] = (self.register_map["ccd3_vrd_config"], "Configured VRD for CCD3") + if PLATO_CAMERA_IS_EM: + primary_header["CCD2_VRD"] = (self.register_map["ccd2_vrd_config"], "Configured VRD for CCD2") + primary_header["CCD3_VRD"] = (self.get_ccd3_vrd(), "Configured VRD for CCD3") + else: + primary_header["CCD2_VRD"] = (self.get_ccd2_vrd(), "Configured VRD for CCD2") + primary_header["CCD3_VRD"] = (self.register_map["ccd3_vrd_config"], "Configured VRD for CCD3") + primary_header["CCD4_VRD"] = (self.register_map["ccd4_vrd_config"], "Configured VRD for CCD4") primary_header["CCD_VOG"] = (self.register_map["ccd_vog_config"], "Configured VOG") primary_header["CCD_VGD"] = (self.get_vgd(), "Configured VGD [V]") @@ -558,18 +592,20 @@

    Module egse.storage.persistence

    primary_header["TRK_HLD_LO"] = (self.register_map["trk_hld_lo"], "Track and hold low") primary_header["CONT_RST_ON"] = (self.register_map["cont_rst_on"], "When 1, FPGA generates continuous reset clock during readout") - primary_header["CONT_CDSCLP_ON"] = (self.register_map["cont_cdsclp_on"], - "When 1, FPGA generates continuous CDS clamp during readout") - primary_header["CONT_ROWCLP_ON"] = (self.register_map["cont_rowclp_on"], - "When 1, FPGA generates continuous row clamp during readout") + if not PLATO_CAMERA_IS_EM: + primary_header["CONT_CDSCLP_ON"] = (self.register_map["cont_cdsclp_on"], + "When 1, FPGA generates continuous CDS clamp during readout") + primary_header["CONT_ROWCLP_ON"] = (self.register_map["cont_rowclp_on"], + "When 1, FPGA generates continuous row clamp during readout") primary_header["R_CFG1"] = (self.register_map["r_cfg1"], "Clock cycle for Rph3-low, Rph1-high") primary_header["R_CFG2"] = (self.register_map["r_cfg2"], "Clock cycle for Rph1-low, Rph2-high") primary_header["CDSCLP_LO"] = (self.register_map["cdsclp_lo"], "Clock cycle for cdsclp low") - primary_header["ADC_PWRDN_EN"] = (self.register_map["adc_pwrdn_en"], - "ADC power-down enabled (0) / disabled (1)") - primary_header["CDSCLP_HI"] = (self.register_map["cdsclp_hi"], "Clock cycle for cdsclp high") - primary_header["ROWCLP_HI"] = (self.register_map["rowclp_hi"], "Clock cycle for rowclp high") - primary_header["ROWCLP_LO"] = (self.register_map["rowclp_lo"], "Clock cycle for rowclp low") + if not PLATO_CAMERA_IS_EM: + primary_header["ADC_PWRDN_EN"] = (self.register_map["adc_pwrdn_en"], + "ADC power-down enabled (0) / disabled (1)") + primary_header["CDSCLP_HI"] = (self.register_map["cdsclp_hi"], "Clock cycle for cdsclp high") + primary_header["ROWCLP_HI"] = (self.register_map["rowclp_hi"], "Clock cycle for rowclp high") + primary_header["ROWCLP_LO"] = (self.register_map["rowclp_lo"], "Clock cycle for rowclp low") # primary_header["SURFACE_INV_CTR"] = (self.register_map["Surface_Inversion_counter"], # "Surface inversion counter") primary_header["READOUT_PAUSE_CTR"] = (self.register_map["Readout_pause_counter"], "Readout pause counter") @@ -589,6 +625,15 @@

    Module egse.storage.persistence

    primary_header["CYCLETIME"] = (self.cycle_time, "Image cycle time [s]") primary_header["READTIME"] = (self.readout_time, "Time needed to read out the requested part for a single CCD side [s]") + + if self.register_map["sync_sel"] == 1: # See https://github.com/IvS-KULeuven/plato-common-egse/issues/2475 + primary_header["READPERIOD"] = (self.cycle_time + INT_SYNC_TIMING_OFFSET, "Time between frames [s] " + "(internal sync)") + texp_cmd = self.cycle_time - self.readout_time + primary_header["TEXP_CMD"] = (texp_cmd, "Commanded exposure time [s] (internal sync)") + primary_header["TEXP_EFF"] = (texp_cmd + INT_SYNC_TIMING_OFFSET, "Effective exposure time [s] (internal " + "sync)") + primary_header["CGSE"] = (self.cgse_version, "Version of the Common EGSE") logger.info(f"Obsid in FITS persistence layer: {self.obsid}") @@ -1327,7 +1372,7 @@

    Module egse.storage.persistence

    def open(self, mode=None): self._mode = mode or self._mode logger.debug(f"Opening file {self._filepath} in mode '{self._mode}'") - self._h5file = h5py.File(self._filepath, mode=self._mode) + self._h5file = h5py.File(self._filepath, mode=self._mode, driver='core') # File "h5py/h5f.pyx", line 554, in h5py.h5f.FileID.start_swmr_write # RuntimeError: Unable to start swmr writing (file superblock version - should be at least 3) @@ -1375,6 +1420,8 @@

    Module egse.storage.persistence

    self._h5file[key] = value.timecode if isinstance(value, HousekeepingPacket): self._h5file[key] = value.packet_as_ndarray + if isinstance(value, HousekeepingData): + self._h5file[key] = value.data_as_ndarray if isinstance(value, DataDataPacket): self._h5file[key] = value.packet_as_ndarray if isinstance(value, OverscanDataPacket): @@ -3367,7 +3414,6 @@

    Args

    # Define the values of the WCS keywords - self.site_name = Settings.load().SITE["ID"] self.v_start = prep["v_start"] # First transmitted row self.v_end = prep["v_end"] # Last transmitted row @@ -3378,11 +3424,12 @@

    Args

    self.obsid = prep["obsid"] self.register_map = prep["register_map"] # Register map - + # Read information from the setup self.setup: Setup = prep["setup"] + self.site_name = self.setup["site_id"] # Site ID self.setup_id = self.setup.get_id() # Setup ID self.camera_id = self.setup.camera.get("ID") # Camera ID (None if not present in the setup) @@ -3440,7 +3487,10 @@

    Args

    return ((vgd_20 << 4) + vgd_19) / 1000 * 5.983 def get_ccd2_vrd(self): - """ Extract the VRD voltage for CCD2 from the register map. + """ + Extract the VRD voltage for CCD2 from the register map. + + NOTE: Use this function only for the FM cameras. Return: Configured VRD voltage. """ @@ -3450,6 +3500,20 @@

    Args

    return int(f'0x{vrd_19:x}{vrd_18:x}', 16) + def get_ccd3_vrd(self): + """ + Extract the VRD voltage for CCD3 from the register map. + + NOTE: Use this function only for the EM camera. + + Return: Configured VRD voltage. + """ + vrd_18 = self.register_map[('reg_18_config', 'ccd3_vrd_config')] + vrd_19 = self.register_map[('reg_19_config', 'ccd3_vrd_config')] + + return int(f'0x{vrd_19:x}{vrd_18:x}', 16) + + def init_data_arrays(self): """ Initialise data arrays in which the data content of the SpW packets will be dumped. @@ -3688,8 +3752,13 @@

    Args

    primary_header["CCD_VOD"] = (self.register_map["ccd_vod_config"], "Configured VOD") primary_header["CCD1_VRD"] = (self.register_map["ccd1_vrd_config"], "Configured VRD for CCD1") - primary_header["CCD2_VRD"] = (self.get_ccd2_vrd(), "Configured VRD for CCD2") - primary_header["CCD3_VRD"] = (self.register_map["ccd3_vrd_config"], "Configured VRD for CCD3") + if PLATO_CAMERA_IS_EM: + primary_header["CCD2_VRD"] = (self.register_map["ccd2_vrd_config"], "Configured VRD for CCD2") + primary_header["CCD3_VRD"] = (self.get_ccd3_vrd(), "Configured VRD for CCD3") + else: + primary_header["CCD2_VRD"] = (self.get_ccd2_vrd(), "Configured VRD for CCD2") + primary_header["CCD3_VRD"] = (self.register_map["ccd3_vrd_config"], "Configured VRD for CCD3") + primary_header["CCD4_VRD"] = (self.register_map["ccd4_vrd_config"], "Configured VRD for CCD4") primary_header["CCD_VOG"] = (self.register_map["ccd_vog_config"], "Configured VOG") primary_header["CCD_VGD"] = (self.get_vgd(), "Configured VGD [V]") @@ -3699,18 +3768,20 @@

    Args

    primary_header["TRK_HLD_LO"] = (self.register_map["trk_hld_lo"], "Track and hold low") primary_header["CONT_RST_ON"] = (self.register_map["cont_rst_on"], "When 1, FPGA generates continuous reset clock during readout") - primary_header["CONT_CDSCLP_ON"] = (self.register_map["cont_cdsclp_on"], - "When 1, FPGA generates continuous CDS clamp during readout") - primary_header["CONT_ROWCLP_ON"] = (self.register_map["cont_rowclp_on"], - "When 1, FPGA generates continuous row clamp during readout") + if not PLATO_CAMERA_IS_EM: + primary_header["CONT_CDSCLP_ON"] = (self.register_map["cont_cdsclp_on"], + "When 1, FPGA generates continuous CDS clamp during readout") + primary_header["CONT_ROWCLP_ON"] = (self.register_map["cont_rowclp_on"], + "When 1, FPGA generates continuous row clamp during readout") primary_header["R_CFG1"] = (self.register_map["r_cfg1"], "Clock cycle for Rph3-low, Rph1-high") primary_header["R_CFG2"] = (self.register_map["r_cfg2"], "Clock cycle for Rph1-low, Rph2-high") primary_header["CDSCLP_LO"] = (self.register_map["cdsclp_lo"], "Clock cycle for cdsclp low") - primary_header["ADC_PWRDN_EN"] = (self.register_map["adc_pwrdn_en"], - "ADC power-down enabled (0) / disabled (1)") - primary_header["CDSCLP_HI"] = (self.register_map["cdsclp_hi"], "Clock cycle for cdsclp high") - primary_header["ROWCLP_HI"] = (self.register_map["rowclp_hi"], "Clock cycle for rowclp high") - primary_header["ROWCLP_LO"] = (self.register_map["rowclp_lo"], "Clock cycle for rowclp low") + if not PLATO_CAMERA_IS_EM: + primary_header["ADC_PWRDN_EN"] = (self.register_map["adc_pwrdn_en"], + "ADC power-down enabled (0) / disabled (1)") + primary_header["CDSCLP_HI"] = (self.register_map["cdsclp_hi"], "Clock cycle for cdsclp high") + primary_header["ROWCLP_HI"] = (self.register_map["rowclp_hi"], "Clock cycle for rowclp high") + primary_header["ROWCLP_LO"] = (self.register_map["rowclp_lo"], "Clock cycle for rowclp low") # primary_header["SURFACE_INV_CTR"] = (self.register_map["Surface_Inversion_counter"], # "Surface inversion counter") primary_header["READOUT_PAUSE_CTR"] = (self.register_map["Readout_pause_counter"], "Readout pause counter") @@ -3730,6 +3801,15 @@

    Args

    primary_header["CYCLETIME"] = (self.cycle_time, "Image cycle time [s]") primary_header["READTIME"] = (self.readout_time, "Time needed to read out the requested part for a single CCD side [s]") + + if self.register_map["sync_sel"] == 1: # See https://github.com/IvS-KULeuven/plato-common-egse/issues/2475 + primary_header["READPERIOD"] = (self.cycle_time + INT_SYNC_TIMING_OFFSET, "Time between frames [s] " + "(internal sync)") + texp_cmd = self.cycle_time - self.readout_time + primary_header["TEXP_CMD"] = (texp_cmd, "Commanded exposure time [s] (internal sync)") + primary_header["TEXP_EFF"] = (texp_cmd + INT_SYNC_TIMING_OFFSET, "Effective exposure time [s] (internal " + "sync)") + primary_header["CGSE"] = (self.cgse_version, "Version of the Common EGSE") logger.info(f"Obsid in FITS persistence layer: {self.obsid}") @@ -5306,8 +5386,13 @@

    Args

    primary_header["CCD_VOD"] = (self.register_map["ccd_vod_config"], "Configured VOD") primary_header["CCD1_VRD"] = (self.register_map["ccd1_vrd_config"], "Configured VRD for CCD1") - primary_header["CCD2_VRD"] = (self.get_ccd2_vrd(), "Configured VRD for CCD2") - primary_header["CCD3_VRD"] = (self.register_map["ccd3_vrd_config"], "Configured VRD for CCD3") + if PLATO_CAMERA_IS_EM: + primary_header["CCD2_VRD"] = (self.register_map["ccd2_vrd_config"], "Configured VRD for CCD2") + primary_header["CCD3_VRD"] = (self.get_ccd3_vrd(), "Configured VRD for CCD3") + else: + primary_header["CCD2_VRD"] = (self.get_ccd2_vrd(), "Configured VRD for CCD2") + primary_header["CCD3_VRD"] = (self.register_map["ccd3_vrd_config"], "Configured VRD for CCD3") + primary_header["CCD4_VRD"] = (self.register_map["ccd4_vrd_config"], "Configured VRD for CCD4") primary_header["CCD_VOG"] = (self.register_map["ccd_vog_config"], "Configured VOG") primary_header["CCD_VGD"] = (self.get_vgd(), "Configured VGD [V]") @@ -5317,18 +5402,20 @@

    Args

    primary_header["TRK_HLD_LO"] = (self.register_map["trk_hld_lo"], "Track and hold low") primary_header["CONT_RST_ON"] = (self.register_map["cont_rst_on"], "When 1, FPGA generates continuous reset clock during readout") - primary_header["CONT_CDSCLP_ON"] = (self.register_map["cont_cdsclp_on"], - "When 1, FPGA generates continuous CDS clamp during readout") - primary_header["CONT_ROWCLP_ON"] = (self.register_map["cont_rowclp_on"], - "When 1, FPGA generates continuous row clamp during readout") + if not PLATO_CAMERA_IS_EM: + primary_header["CONT_CDSCLP_ON"] = (self.register_map["cont_cdsclp_on"], + "When 1, FPGA generates continuous CDS clamp during readout") + primary_header["CONT_ROWCLP_ON"] = (self.register_map["cont_rowclp_on"], + "When 1, FPGA generates continuous row clamp during readout") primary_header["R_CFG1"] = (self.register_map["r_cfg1"], "Clock cycle for Rph3-low, Rph1-high") primary_header["R_CFG2"] = (self.register_map["r_cfg2"], "Clock cycle for Rph1-low, Rph2-high") primary_header["CDSCLP_LO"] = (self.register_map["cdsclp_lo"], "Clock cycle for cdsclp low") - primary_header["ADC_PWRDN_EN"] = (self.register_map["adc_pwrdn_en"], - "ADC power-down enabled (0) / disabled (1)") - primary_header["CDSCLP_HI"] = (self.register_map["cdsclp_hi"], "Clock cycle for cdsclp high") - primary_header["ROWCLP_HI"] = (self.register_map["rowclp_hi"], "Clock cycle for rowclp high") - primary_header["ROWCLP_LO"] = (self.register_map["rowclp_lo"], "Clock cycle for rowclp low") + if not PLATO_CAMERA_IS_EM: + primary_header["ADC_PWRDN_EN"] = (self.register_map["adc_pwrdn_en"], + "ADC power-down enabled (0) / disabled (1)") + primary_header["CDSCLP_HI"] = (self.register_map["cdsclp_hi"], "Clock cycle for cdsclp high") + primary_header["ROWCLP_HI"] = (self.register_map["rowclp_hi"], "Clock cycle for rowclp high") + primary_header["ROWCLP_LO"] = (self.register_map["rowclp_lo"], "Clock cycle for rowclp low") # primary_header["SURFACE_INV_CTR"] = (self.register_map["Surface_Inversion_counter"], # "Surface inversion counter") primary_header["READOUT_PAUSE_CTR"] = (self.register_map["Readout_pause_counter"], "Readout pause counter") @@ -5348,6 +5435,15 @@

    Args

    primary_header["CYCLETIME"] = (self.cycle_time, "Image cycle time [s]") primary_header["READTIME"] = (self.readout_time, "Time needed to read out the requested part for a single CCD side [s]") + + if self.register_map["sync_sel"] == 1: # See https://github.com/IvS-KULeuven/plato-common-egse/issues/2475 + primary_header["READPERIOD"] = (self.cycle_time + INT_SYNC_TIMING_OFFSET, "Time between frames [s] " + "(internal sync)") + texp_cmd = self.cycle_time - self.readout_time + primary_header["TEXP_CMD"] = (texp_cmd, "Commanded exposure time [s] (internal sync)") + primary_header["TEXP_EFF"] = (texp_cmd + INT_SYNC_TIMING_OFFSET, "Effective exposure time [s] (internal " + "sync)") + primary_header["CGSE"] = (self.cgse_version, "Version of the Common EGSE") logger.info(f"Obsid in FITS persistence layer: {self.obsid}") @@ -5454,13 +5550,17 @@

    Returns

    Extract the VRD voltage for CCD2 from the register map.

    +

    NOTE: Use this function only for the FM cameras.

    Return: Configured VRD voltage.

    Expand source code
    def get_ccd2_vrd(self):
    -    """ Extract the VRD voltage for CCD2 from the register map.
    +    """
    +    Extract the VRD voltage for CCD2 from the register map.
    +
    +    NOTE: Use this function only for the FM cameras.
     
         Return: Configured VRD voltage.
         """
    @@ -5470,6 +5570,31 @@ 

    Returns

    return int(f'0x{vrd_19:x}{vrd_18:x}', 16)
    +
    +def get_ccd3_vrd(self) +
    +
    +

    Extract the VRD voltage for CCD3 from the register map.

    +

    NOTE: Use this function only for the EM camera.

    +

    Return: Configured VRD voltage.

    +
    + +Expand source code + +
    def get_ccd3_vrd(self):
    +    """
    +    Extract the VRD voltage for CCD3 from the register map.
    +
    +    NOTE: Use this function only for the EM camera.
    +
    +    Return: Configured VRD voltage.
    +    """
    +    vrd_18 = self.register_map[('reg_18_config', 'ccd3_vrd_config')]
    +    vrd_19 = self.register_map[('reg_19_config', 'ccd3_vrd_config')]
    +
    +    return int(f'0x{vrd_19:x}{vrd_18:x}', 16)
    +
    +
    def get_vgd(self)
    @@ -5752,7 +5877,7 @@

    Ancestors

    def open(self, mode=None): self._mode = mode or self._mode logger.debug(f"Opening file {self._filepath} in mode '{self._mode}'") - self._h5file = h5py.File(self._filepath, mode=self._mode) + self._h5file = h5py.File(self._filepath, mode=self._mode, driver='core') # File "h5py/h5f.pyx", line 554, in h5py.h5f.FileID.start_swmr_write # RuntimeError: Unable to start swmr writing (file superblock version - should be at least 3) @@ -5800,6 +5925,8 @@

    Ancestors

    self._h5file[key] = value.timecode if isinstance(value, HousekeepingPacket): self._h5file[key] = value.packet_as_ndarray + if isinstance(value, HousekeepingData): + self._h5file[key] = value.data_as_ndarray if isinstance(value, DataDataPacket): self._h5file[key] = value.packet_as_ndarray if isinstance(value, OverscanDataPacket): @@ -5938,6 +6065,8 @@

    Returns

    self._h5file[key] = value.timecode if isinstance(value, HousekeepingPacket): self._h5file[key] = value.packet_as_ndarray + if isinstance(value, HousekeepingData): + self._h5file[key] = value.data_as_ndarray if isinstance(value, DataDataPacket): self._h5file[key] = value.packet_as_ndarray if isinstance(value, OverscanDataPacket): @@ -6854,6 +6983,7 @@

    create_primary_header
  • extract_full_image_mode
  • get_ccd2_vrd
  • +
  • get_ccd3_vrd
  • get_vgd
  • got_all_last_packets
  • init_data_arrays
  • diff --git a/docs/api/egse/storage/storage_cs.html b/docs/api/egse/storage/storage_cs.html index b26c953..4f81e5d 100644 --- a/docs/api/egse/storage/storage_cs.html +++ b/docs/api/egse/storage/storage_cs.html @@ -83,6 +83,20 @@

    Module egse.storage.storage_cs

    self.poller.register(self.dev_ctrl_cmd_sock, zmq.POLLIN) + from egse.confman import ConfigurationManagerProxy, is_configuration_manager_active + from egse.listener import EVENT_ID + + self.register_as_listener( + proxy=ConfigurationManagerProxy, + listener={'name': 'Storage CS', 'proxy': StorageProxy, 'event_id': EVENT_ID.SETUP} + ) + + # NOTE: + # Since the CM CS is started after the SM CS in the normal startup sequence, delay the task to load + # the Setup until after the CM CS has been properly started. We do that now with a delay time of 30s, + # but we might in the future use a function returning a boolean, until... + self.schedule_task(self.device_protocol.controller.load_setup, after=10.0, when=is_configuration_manager_active) + def before_serve(self): self.scheduler = BackgroundScheduler(timezone=utc) @@ -92,7 +106,10 @@

    Module egse.storage.storage_cs

    start_http_server(CTRL_SETTINGS.METRICS_PORT) def after_serve(self): + from egse.confman import ConfigurationManagerProxy + self.scheduler.shutdown() + self.unregister_as_listener(proxy=ConfigurationManagerProxy, listener={'name': 'Storage CS'}) def get_communication_protocol(self): return CTRL_SETTINGS.PROTOCOL @@ -172,6 +189,7 @@

    Module egse.storage.storage_cs

    rich.print(f" Commanding port: {sm.get_commanding_port()}") rich.print(f" Service port: {sm.get_service_port()}") rich.print(f" Storage location: {sm.get_storage_location()}") + rich.print(f" Loaded Setup: {sm.get_loaded_setup_id()}") rich.print(f" Registrations: {sm.get_registry_names()}") if full: rich.print("Filenames for all registered items:") @@ -187,7 +205,6 @@

    Module egse.storage.storage_cs

    rich.print(f"Total disk space: {humanize_bytes(total)}") rich.print(f"Used disk space: {humanize_bytes(used)} ({(used / total * 100):.2f}%)") rich.print(f"Free disk space: {humanize_bytes(free)} ({(free / total * 100):.2f}%)") - else: rich.print(" Status: [red]not active") @@ -221,7 +238,7 @@

    Module egse.storage.storage_cs

    ) fails += 1 - logger.debug(f"location = {location}") + # logger.debug(f"Storage location for data = {location}") daily_dir = location / "daily" obs_dir = location / "obs" @@ -293,7 +310,7 @@

    Raises

    ) fails += 1 - logger.debug(f"location = {location}") + # logger.debug(f"Storage location for data = {location}") daily_dir = location / "daily" obs_dir = location / "obs" @@ -345,6 +362,20 @@

    Classes

    self.poller.register(self.dev_ctrl_cmd_sock, zmq.POLLIN) + from egse.confman import ConfigurationManagerProxy, is_configuration_manager_active + from egse.listener import EVENT_ID + + self.register_as_listener( + proxy=ConfigurationManagerProxy, + listener={'name': 'Storage CS', 'proxy': StorageProxy, 'event_id': EVENT_ID.SETUP} + ) + + # NOTE: + # Since the CM CS is started after the SM CS in the normal startup sequence, delay the task to load + # the Setup until after the CM CS has been properly started. We do that now with a delay time of 30s, + # but we might in the future use a function returning a boolean, until... + self.schedule_task(self.device_protocol.controller.load_setup, after=10.0, when=is_configuration_manager_active) + def before_serve(self): self.scheduler = BackgroundScheduler(timezone=utc) @@ -354,7 +385,10 @@

    Classes

    start_http_server(CTRL_SETTINGS.METRICS_PORT) def after_serve(self): + from egse.confman import ConfigurationManagerProxy + self.scheduler.shutdown() + self.unregister_as_listener(proxy=ConfigurationManagerProxy, listener={'name': 'Storage CS'}) def get_communication_protocol(self): return CTRL_SETTINGS.PROTOCOL @@ -384,7 +418,10 @@

    Methods

    Expand source code
    def after_serve(self):
    -    self.scheduler.shutdown()
    + from egse.confman import ConfigurationManagerProxy + + self.scheduler.shutdown() + self.unregister_as_listener(proxy=ConfigurationManagerProxy, listener={'name': 'Storage CS'})
    @@ -462,10 +499,15 @@

    Inherited members

    • ControlServer:
    • diff --git a/docs/api/egse/synoptics/index.html b/docs/api/egse/synoptics/index.html index 37f64c8..379dd6f 100644 --- a/docs/api/egse/synoptics/index.html +++ b/docs/api/egse/synoptics/index.html @@ -46,7 +46,7 @@

      Module egse.synoptics

      from egse.protocol import CommandProtocol from egse.proxy import Proxy from egse.settings import Settings -from egse.state import GlobalState +from egse.setup import load_setup, Setup from egse.storage import StorageProxy from egse.storage import is_storage_manager_active from egse.storage import register_to_storage_manager @@ -194,6 +194,8 @@

      Module egse.synoptics

      """Initialisation for the Synoptics Manager Controller. """ + self.setup = load_setup() + # Storage Manager must be active if not is_storage_manager_active(): @@ -211,8 +213,8 @@

      Module egse.synoptics

      self.synoptics_metrics = None def read_hk_info(self): - self.syn_names, self.original_name_egse, self.original_name_th = read_hk_info() - self.synoptics_metrics = define_metrics(ORIGIN) + self.syn_names, self.original_name_egse, self.original_name_th = read_hk_info(self.setup) + self.synoptics_metrics = define_metrics(ORIGIN, setup=self.setup) self.hk_info_read = True @@ -311,16 +313,19 @@

      Module egse.synoptics

      logger.debug(f"Cannot store synoptics metrics: {name} is not a synoptical parameter") -def read_hk_info() -> (dict, dict): +def read_hk_info(setup: Setup) -> (dict, dict): """ Read the table with the HK information, i.e. the TM dictionary. + Args: + - setup: Setup + Returns: - List with the name of the timestamp and synoptics - Dictionary with the original name at the TH you are currently at - Dictionary with the original name from the EGSE (not TH-specific) """ - hk_info_table = GlobalState.setup.telemetry.dictionary + hk_info_table = setup.telemetry.dictionary syn_selection = np.where(hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values == ORIGIN) syn_names = hk_info_table[TmDictionaryColumns.CORRECT_HK_NAMES].values[syn_selection] @@ -580,10 +585,14 @@

      Returns

      -def read_hk_info() ‑> () +def read_hk_info(setup: Setup) ‑> ()

      Read the table with the HK information, i.e. the TM dictionary.

      +

      Args

      +
        +
      • setup: Setup
      • +

      Returns

      • List with the name of the timestamp and synoptics
      • @@ -594,16 +603,19 @@

        Returns

        Expand source code -
        def read_hk_info() -> (dict, dict):
        +
        def read_hk_info(setup: Setup) -> (dict, dict):
             """ Read the table with the HK information, i.e. the TM dictionary.
         
        +    Args:
        +        - setup: Setup
        +
             Returns:
                 - List with the name of the timestamp and synoptics
                 - Dictionary with the original name at the TH you are currently at
                 - Dictionary with the original name from the EGSE (not TH-specific)
             """
         
        -    hk_info_table = GlobalState.setup.telemetry.dictionary
        +    hk_info_table = setup.telemetry.dictionary
         
             syn_selection = np.where(hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values == ORIGIN)
             syn_names = hk_info_table[TmDictionaryColumns.CORRECT_HK_NAMES].values[syn_selection]
        @@ -688,6 +700,8 @@ 

        Inherited members

        """Initialisation for the Synoptics Manager Controller. """ + self.setup = load_setup() + # Storage Manager must be active if not is_storage_manager_active(): @@ -705,8 +719,8 @@

        Inherited members

        self.synoptics_metrics = None def read_hk_info(self): - self.syn_names, self.original_name_egse, self.original_name_th = read_hk_info() - self.synoptics_metrics = define_metrics(ORIGIN) + self.syn_names, self.original_name_egse, self.original_name_th = read_hk_info(self.setup) + self.synoptics_metrics = define_metrics(ORIGIN, setup=self.setup) self.hk_info_read = True @@ -820,8 +834,8 @@

        Methods

        Expand source code
        def read_hk_info(self):
        -    self.syn_names, self.original_name_egse, self.original_name_th = read_hk_info()
        -    self.synoptics_metrics = define_metrics(ORIGIN)
        +    self.syn_names, self.original_name_egse, self.original_name_th = read_hk_info(self.setup)
        +    self.synoptics_metrics = define_metrics(ORIGIN, setup=self.setup)
         
             self.hk_info_read = True
        diff --git a/docs/api/egse/synoptics/syn_cs.html b/docs/api/egse/synoptics/syn_cs.html index a05d1d8..8be53fd 100644 --- a/docs/api/egse/synoptics/syn_cs.html +++ b/docs/api/egse/synoptics/syn_cs.html @@ -547,10 +547,15 @@

        Inherited members

        • ControlServer:
        • diff --git a/docs/api/egse/system.html b/docs/api/egse/system.html index ebb3d69..39db158 100644 --- a/docs/api/egse/system.html +++ b/docs/api/egse/system.html @@ -44,12 +44,13 @@

          Module egse.system

          * __psutil__: for system statistics """ -import collections -import math +from __future__ import annotations import builtins +import collections import contextlib import datetime +import functools import importlib import inspect import itertools @@ -65,24 +66,23 @@

          Module egse.system

          import time from collections import namedtuple from pathlib import Path +from types import FunctionType +from types import ModuleType from typing import Any from typing import Callable from typing import Iterable from typing import List -from typing import NamedTuple from typing import Optional from typing import Tuple from typing import Union import distro # For determining the Linux distribution import psutil -import rich from rich.text import Text from rich.tree import Tree - EPOCH_1958_1970 = 378691200 -TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%f%z' +TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f%z" logger = logging.getLogger(__name__) @@ -92,16 +92,28 @@

          Module egse.system

          # Code below is copied from https://gist.github.com/simon-weber/7853144 + @contextmanager def all_logging_disabled(highest_level=logging.CRITICAL, flag=True): """ - A context manager that will prevent any logging messages triggered during the body from being processed. + Context manager to temporarily disable logging messages during its execution. Args: - highest_level: the maximum logging level in use. - This would only need to be changed if a custom level greater than CRITICAL is defined. - flag: True to disable all logging [default=True] + highest_level (int, optional): The maximum logging level to be disabled. + Defaults to logging.CRITICAL. + Note: Adjust this only if a custom level greater than CRITICAL is defined. + flag (bool, optional): If True, disables all logging; if False, no changes are made. + Defaults to True. + + Example: + >>> with all_logging_disabled(): + ... # Your code with logging messages disabled + + Note: + This context manager is designed to prevent any logging messages triggered during its body + from being processed. It temporarily disables logging and restores the previous state afterward. """ + # two kind-of hacks here: # * can't get the highest logging level in effect => delegate to the user # * can't get the current module-level override => use an undocumented @@ -119,20 +131,35 @@

          Module egse.system

          def get_active_loggers() -> dict: + """ + Retrieves information about active loggers and their respective log levels. + + Returns a dictionary where keys are the names of active loggers, and values + are the corresponding log levels in string format. + + Returns: + dict: A dictionary mapping logger names to their log levels. + + Note: + This function provides a snapshot of the currently active loggers and + their log levels at the time of the function call. + + """ + return { - name: logging.getLevelName(logging.getLogger(name).level) - for name in sorted(logging.Logger.manager.loggerDict) + name: logging.getLevelName(logging.getLogger(name).level) for name in sorted(logging.Logger.manager.loggerDict) } # The code below was taken from https://stackoverflow.com/a/69639238/4609203 + def ignore_m_warning(modules=None): """ Ignore RuntimeWarning by `runpy` that occurs when executing a module with `python -m package.module`, while that module is also imported. - The original warning mssage is: + The original warning message is: '<package.module>' found in sys.modules after import of package '<package'>, but prior to execution of '<package.module>' @@ -147,12 +174,20 @@

          Module egse.system

          msg = "'{module}' found in sys.modules after import of package" for module in modules: module_msg = re.escape(msg.format(module=module)) - warnings.filterwarnings("ignore", message=module_msg, category=RuntimeWarning, module='runpy') # ignore -m + warnings.filterwarnings("ignore", message=module_msg, category=RuntimeWarning, module="runpy") # ignore -m except (ImportError, KeyError, AttributeError, Exception): pass -def format_datetime(dt: Union[str,datetime.datetime] = None, fmt: str = None, width: int = 6, precision: int = 3): +def now(utc: bool = True): + """Returns a datetime object for the current time in UTC or local time.""" + if utc: + return datetime.datetime.now(tz=datetime.timezone.utc) + else: + return datetime.datetime.now() + + +def format_datetime(dt: Union[str, datetime.datetime] = None, fmt: str = None, width: int = 6, precision: int = 3): """Format a datetime as YYYY-mm-ddTHH:MM:SS.μs+0000. If the given argument is not timezone aware, the last part, i.e. `+0000` will not be there. @@ -224,7 +259,7 @@

          Module egse.system

          SECONDS_IN_A_MINUTE = 60 -def humanize_seconds(seconds: float): +def humanize_seconds(seconds: float, include_micro_seconds: bool = True): """ The number of seconds is represented as "[#D]d [#H]h[#M]m[#S]s.MS" where: @@ -241,6 +276,8 @@

          Module egse.system

          '10d 00s.000' >>> humanize_seconds(10*86400 + 3*3600 + 42.023) '10d 03h00m42s.023' + >>> humanize_seconds(10*86400 + 3*3600 + 42.023, include_micro_seconds=False) + '10d 03h00m42s' Returns: a string representation for the number of seconds. @@ -270,13 +307,14 @@

          Module egse.system

          result += f"{minutes:02d}m" result += f"{seconds:02d}s" - result += f".{micro_seconds:03d}" + if include_micro_seconds: + result += f".{micro_seconds:03d}" return result def str_to_datetime(datetime_string: str): - """ Convert the given string to a datetime object. + """Convert the given string to a datetime object. Args: - datatime_string: String representing a datetime, in the format %Y-%m-%dT%H:%M:%S.%f%z. @@ -287,8 +325,34 @@

          Module egse.system

          return datetime.datetime.strptime(datetime_string.strip("\r"), TIME_FORMAT) +def duration(dt_start: str | datetime.datetime, dt_end: str | datetime.datetime) -> datetime.timedelta: + """ + Returns a `timedelta` object with the duration, i.e. time difference between dt_start and dt_end. + + Notes: + If you need the number of seconds of your measurement, use the `total_seconds()` method of + the timedelta object. + + Even if you —by accident— switch the start and end time arguments, the duration will + be calculated as expected. + + Args: + dt_start: start time of the measurement + dt_end: end time of the measurement + + Returns: + The time difference (duration) between dt_start and dt_end. + """ + if isinstance(dt_start, str): + dt_start = str_to_datetime(dt_start) + if isinstance(dt_end, str): + dt_end = str_to_datetime(dt_end) + + return dt_end - dt_start if dt_end > dt_start else dt_start - dt_end + + def time_since_epoch_1958(datetime_string: str): - """ Calculate the time since epoch 1958 for the given string representation of a datetime. + """Calculate the time since epoch 1958 for the given string representation of a datetime. Args: - datetime_string: String representing a datetime, in the format %Y-%m-%dT%H:%M:%S.%f%z. @@ -296,7 +360,7 @@

          Module egse.system

          Returns: Time since the 1958 epoch [s]. """ - time_since_epoch_1970 = str_to_datetime(datetime_string).timestamp() # Since Jan 1st, 1970, midnight + time_since_epoch_1970 = str_to_datetime(datetime_string).timestamp() # Since Jan 1st, 1970, midnight return time_since_epoch_1970 + EPOCH_1958_1970 @@ -336,6 +400,10 @@

          Module egse.system

          self.name = name self.precision = precision self.log_level = log_level + caller_info = get_caller_info(level=2) + self.filename = caller_info.filename + self.func = caller_info.function + self.lineno = caller_info.lineno def __enter__(self): # start is a value containing the start time in fractional seconds @@ -351,8 +419,8 @@

          Module egse.system

          # Overwrite self.end() so that it always returns the fixed end time self.end = self._end - logger.log(self.log_level, - f"{self.name}: {self.end() - self.start:0.{self.precision}f} seconds") + logger.log(self.log_level, f"{self.name} [{self.filename}:{self.func}:{self.lineno}]: " + f"{self.end() - self.start:0.{self.precision}f} seconds") return False def __call__(self): @@ -360,8 +428,8 @@

          Module egse.system

          def log_elapsed(self): """Sends the elapsed time info to the default logger.""" - logger.log(self.log_level, - f"{self.name}: {self.end() - self.start:0.{self.precision}f} seconds elapsed") + logger.log(self.log_level, f"{self.name} [{self.func}:{self.lineno}]: " + f"{self.end() - self.start:0.{self.precision}f} seconds elapsed") def get_elapsed(self) -> float: """Returns the elapsed time for this timer as a float in seconds.""" @@ -410,8 +478,8 @@

          Module egse.system

          try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - #sock.connect(("8.8.8.8", 80)) - sock.connect(('10.255.255.255', 1)) + # sock.connect(("8.8.8.8", 80)) + sock.connect(("10.255.255.255", 1)) host_ip = sock.getsockname()[0] sock.close() except Exception as exc: @@ -425,11 +493,14 @@

          Module egse.system

          host_name = socket.gethostname() host_ip = socket.gethostbyname(host_name) return host_ip - except (Exception, ): + except (Exception,): return None -def get_caller_info(level=1) -> NamedTuple: +CallerInfo = namedtuple("CallerInfo", "filename function lineno") + + +def get_caller_info(level=1) -> CallerInfo: """ Returns the filename, function name and lineno of the caller. @@ -452,9 +523,7 @@

          Module egse.system

          frame = frame.f_back frame_info = inspect.getframeinfo(frame) - caller_info = namedtuple("CallerInfo", "filename function lineno") - - return caller_info(frame_info.filename, frame_info.function, frame_info.lineno) + return CallerInfo(frame_info.filename, frame_info.function, frame_info.lineno) def get_referenced_var_name(obj: Any) -> List[str]: @@ -515,7 +584,6 @@

          Module egse.system

          return tree def __repr__(self): - # We only want the first 10 key:value pairs count = 10 @@ -660,9 +728,9 @@

          Module egse.system

          boot_time = psutil.boot_time() statistics["boot_time"] = boot_time - statistics["since"] = datetime.datetime.fromtimestamp( - boot_time, tz=datetime.timezone.utc - ).strftime("%Y-%m-%d %H:%M:%S") + statistics["since"] = datetime.datetime.fromtimestamp(boot_time, tz=datetime.timezone.utc).strftime( + "%Y-%m-%d %H:%M:%S" + ) return statistics @@ -778,6 +846,64 @@

          Module egse.system

          return False +def waiting_for(condition, *args, interval=0.1, timeout=1, verbose=False, **kwargs): + """ + Sleep until the given condition is fulfilled. The arguments are passed into the condition + callable which is called in a while loop until the condition is met or the timeout is reached. + + Note that the condition can be a function, method or callable class object. + An example of the latter is: + + class SleepUntilCount: + def __init__(self, end): + self._end = end + self._count = 0 + + def __call__(self, *args, **kwargs): + self._count += 1 + if self._count >= self._end: + return True + else: + return False + + + Args: + condition: a callable that returns True when the condition is met, False otherwise + interval: the sleep interval between condition checks [s, default=0.1] + timeout: the period after which the function returns, even when the condition is + not met [s, default=1] + verbose: log debugging messages if True + *args: any arguments that will be passed into the condition function + + Raises: + A TimeoutError when the condition was not fulfilled within the timeout period. + """ + + if inspect.isfunction(condition) or inspect.ismethod(condition): + func_name = condition.__name__ + else: + func_name = condition.__class__.__name__ + + caller = get_caller_info(level=2) + + start = time.time() + + while not condition(*args, **kwargs): + if time.time() - start > timeout: + raise TimeoutError( + f"Timeout after {timeout} sec, from {caller.filename} at {caller.lineno}," + f" {func_name}{args} not met." + ) + time.sleep(interval) + + duration = time.time() - start + + if verbose: + logger.debug(f"waiting_for finished successfully after {duration:.3f}s, {func_name}{args}{kwargs} is met.") + + return duration + + def has_internet(host="8.8.8.8", port=53, timeout=3): """Returns True if we have internet connection. @@ -959,11 +1085,7 @@

          Module egse.system

          value = (operator.eq, value) attr_func_values.append((attr.replace("__", "."), *value)) - return [ - el - for el in elements - if all(check(attr, func, value, el) for attr, func, value in attr_func_values) - ] + return [el for el in elements if all(check(attr, func, value, el) for attr, func, value in attr_func_values)] def replace_environment_variable(input_string: str): @@ -1007,7 +1129,6 @@

          Module egse.system

          filename = Path(filename) if not filename.exists(): - return None with filename.open("rb") as file: @@ -1021,7 +1142,7 @@

          Module egse.system

          def read_last_lines(filename: str, num_lines: int): - """ Return the last lines of a text file. + """Return the last lines of a text file. Args: - filename: Filename. @@ -1038,7 +1159,6 @@

          Module egse.system

          assert num_lines > 1 if not filename.exists(): - return None assert num_lines >= 0 @@ -1052,20 +1172,15 @@

          Module egse.system

          lines = [] with open(filename) as f: - while len(lines) <= num_lines: - try: - f.seek(-pos, 2) except IOError: - f.seek(0) break finally: - lines = list(f) # Increasing value of variable exponentially @@ -1075,11 +1190,141 @@

          Module egse.system

          return lines[-num_lines:] +def is_namespace(module) -> bool: + """ + Checks if a module represents a namespace package. + + A namespace package is defined as a module that has a '__path__' attribute + and no '__file__' attribute. + + Args: + module: The module to be checked. + + Returns: + bool: True if the module is a namespace package, False otherwise. + + Note: + A namespace package is a special kind of package that does not contain + an actual implementation but serves as a container for other packages + or modules. + + """ + + if hasattr(module, '__path__') and getattr(module, '__file__', None) is None: + return True + else: + return False + + +def get_package_location(module) -> List[Path]: + """ + Retrieves the file system locations associated with a Python package. + + This function takes a module, module name, or fully qualified module path, + and returns a list of Path objects representing the file system locations + associated with the package. If the module is a namespace package, it returns + the paths of all namespaces; otherwise, it returns the location of the module. + + Args: + module (Union[FunctionType, ModuleType, str]): The module or module name to + retrieve locations for. + + Returns: + List[Path]: A list of Path objects representing the file system locations. + + Note: + If the module is not found or is not a valid module, an empty list is returned. + + """ + + if isinstance(module, FunctionType): + module_name = module.__module__ + elif isinstance(module, ModuleType): + module_name = module.__name__ + elif isinstance(module, str): + module_name = module + else: + return [] + + try: + module = importlib.import_module(module) + except TypeError as exc: + return [] + + if is_namespace(module): + return [ + Path(location) + for location in module.__path__ + ] + else: + location = get_module_location(module) + return [] if location is None else [location] + + +def get_module_location(arg) -> Optional[Path]: + """ + Returns the location of the module as a Path object. + + The function accepts a string (module name), a function, or a module. + For functions and modules, the module name is determined internally. + + Args: + arg (Union[FunctionType, ModuleType, str]): The function, module, or module name. + + Returns: + Optional[Path]: The location of the module as a Path object, or None if the location + cannot be determined or an invalid argument is provided. + + Example: + >>> get_module_location('egse') + Path('/path/to/egse') + + >>> get_module_location(egse.system) + Path('/path/to/egse/system') + + Note: + If the module is not found or is not a valid module, None is returned. + + """ + if isinstance(arg, FunctionType): + module_name = arg.__module__ + elif isinstance(arg, ModuleType): + module_name = arg.__name__ + elif isinstance(arg, str): + module_name = arg + else: + return None + + try: + module = importlib.import_module(module_name) + except TypeError as exc: + return None + + return Path(module.__file__).parent.resolve() + + def get_full_classname(obj: object) -> str: - """Returns the fully qualified class name for this object.""" + """ + Returns the fully qualified class name for the given object. + + Args: + obj (object): The object for which to retrieve the fully qualified class name. + + Returns: + str: The fully qualified class name, including the module. + + Example: + >>> get_full_classname("example") + 'builtins.str' - # Take into account that obj might be a class or a builtin or even a - # literal like an int or a float or a complex number + >>> get_full_classname(42) + 'builtins.int' + + Note: + The function considers various scenarios, such as objects being classes, + built-ins, or literals like int, float, or complex numbers. + + """ if type(obj) is type or obj.__class__.__module__ == str.__module__: try: @@ -1092,7 +1337,7 @@

          Module egse.system

          module = obj.__class__.__module__ name = obj.__class__.__qualname__ - return module + '.' + name + return module + "." + name def find_class(class_name: str): @@ -1119,12 +1364,11 @@

          Module egse.system

          def type_name(var): - """ Returns the name of the type of var.""" + """Returns the name of the type of var.""" return type(var).__name__ -def check_argument_type( - obj: object, name: str, target_class: Union[type, Tuple[type]], allow_none: bool = False): +def check_argument_type(obj: object, name: str, target_class: Union[type, Tuple[type]], allow_none: bool = False): """Check that the given object is of a specific (sub)type of the given target_class. The target_class can be a tuple of types. @@ -1143,12 +1387,31 @@

          Module egse.system

          def check_str_for_slash(arg: str): """Check if there is a slash in the given string, and raise a ValueError if so.""" - if '/' in arg: + if "/" in arg: ValueError(f"The given argument can not contain slashes, {arg=}.") def check_is_a_string(var, allow_none=False): - """ Calls is_a_string and raises a type error if the check fails.""" + """ + Checks if the given variable is a string and raises a TypeError if the check fails. + + Args: + var: The variable to be checked. + allow_none (bool, optional): If True, allows the variable to be None without raising an error. + Defaults to False. + + Raises: + TypeError: If the variable is not a string or is None (when allow_none is False). + + Example: + >>> check_is_a_string("example") + + Note: + This function is designed to validate that the input variable is a string. + If `allow_none` is set to True, it allows the variable to be None without raising an error. + + """ + if var is None and allow_none: return if var is None and not allow_none: @@ -1159,27 +1422,44 @@

          Module egse.system

          def sanity_check(flag: bool, msg: str): """ - This is a replacement for the 'assert' statement. Use this in production code - such that your checks are not removed during optimisations. + Checks a boolean flag and raises an AssertionError with the provided message if the check fails. + + This function serves as a replacement for the 'assert' statement in production code. + Using this ensures that your checks are not removed during optimizations. + + Args: + flag (bool): The boolean flag to be checked. + msg (str): The message to be included in the AssertionError if the check fails. + + Raises: + AssertionError: If the flag is False. + + Example: + >>> sanity_check(x > 0, "x must be greater than 0") + + Note: + This function is designed for production code to perform runtime checks + that won't be removed during optimizations. + """ + if not flag: raise AssertionError(msg) class NotSpecified: - """ Class for NOT_SPECIFIED constant. - Is used so that a parameter can have a default value other than None. + """Class for NOT_SPECIFIED constant. + Is used so that a parameter can have a default value other than None. - Evaluate to False when converted to boolean. + Evaluate to False when converted to boolean. """ + def __nonzero__(self): - """ Always returns False. Called when to converting to bool in Python 2. - """ + """Always returns False. Called when to converting to bool in Python 2.""" return False def __bool__(self): - """ Always returns False. Called when to converting to bool in Python 3. - """ + """Always returns False. Called when to converting to bool in Python 3.""" return False @@ -1271,6 +1551,23 @@

          Module egse.system

          _function_timing = {} +def execution_time(func): + """ + A decorator to save the execution time of the function. Use this decorator + if you want —by default and always— have an idea of the average execution time + of the given function. + + Use this in conjunction with the `get_average_execution_time()` function to + retrieve the average execution time for the given function. + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + return save_average_execution_time(func, *args, **kwargs) + + return wrapper + + def save_average_execution_time(func: Callable, *args, **kwargs): """ Executes the function 'func' with the given arguments and saves the execution time. All positional @@ -1294,6 +1591,7 @@

          Module egse.system

          """ Returns the average execution time of the given function. The function 'func' shall be previously executed using the `save_average_execution_time()` function which remembers the last 100 execution times of the function. + You can also decorate your function with `@execution_time` to permanently monitor it. The average time is a moving average over the last 100 times. If the function was never called before, 0.0 is returned. @@ -1302,6 +1600,12 @@

          Module egse.system

          stalled. """ + # If the function was previously wrapped with the `@execution_time` wrapper, we need to get + # to the original function object because that's the one that is saved. + + with contextlib.suppress(AttributeError): + func = func.__wrapped__ + try: d = _function_timing[func] return sum(d) / len(d) @@ -1309,14 +1613,36 @@

          Module egse.system

          return 0.0 -ignore_m_warning('egse.system') +def get_average_execution_times() -> dict: + """ + Returns a dictionary with "function name": average execution time, for all function that have been + monitored in this process. + """ + return {func.__name__: get_average_execution_time(func) for func in _function_timing} -if __name__ == "__main__": +def clear_average_execution_times(): + """Clear out all function timing for this process.""" + _function_timing.clear() + + +def get_system_architecture() -> str: + """ + Returns the machine type. This is a string describing the processor architecture, + like i386 or arm64, but the exact string is not defined. An empty string can be returned when + the type cannot be determined. + """ + return platform.machine() + + +ignore_m_warning("egse.system") + +if __name__ == "__main__": print(f"Host IP: {get_host_ip()}") print(f"System name: {get_system_name()}") print(f"OS name: {get_os_name()}") print(f"OS version: {get_os_version()}") + print(f"Architecture: {get_system_architecture()}") print(f"Python version: {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}") print("Running in IPython") if is_in_ipython() else None
        @@ -1332,15 +1658,24 @@

        Functions

        def all_logging_disabled(highest_level=50, flag=True)
    -

    A context manager that will prevent any logging messages triggered during the body from being processed.

    +

    Context manager to temporarily disable logging messages during its execution.

    Args

    -
    highest_level
    -
    the maximum logging level in use. -This would only need to be changed if a custom level greater than CRITICAL is defined.
    -
    flag
    -
    True to disable all logging [default=True]
    -
    +
    highest_level : int, optional
    +
    The maximum logging level to be disabled. +Defaults to logging.CRITICAL. +Note: Adjust this only if a custom level greater than CRITICAL is defined.
    +
    flag : bool, optional
    +
    If True, disables all logging; if False, no changes are made. +Defaults to True.
    + +

    Example

    +
    >>> with all_logging_disabled():
    +...     # Your code with logging messages disabled
    +
    +

    Note

    +

    This context manager is designed to prevent any logging messages triggered during its body +from being processed. It temporarily disables logging and restores the previous state afterward.

    Expand source code @@ -1348,13 +1683,24 @@

    Args

    @contextmanager
     def all_logging_disabled(highest_level=logging.CRITICAL, flag=True):
         """
    -    A context manager that will prevent any logging messages triggered during the body from being processed.
    +    Context manager to temporarily disable logging messages during its execution.
     
         Args:
    -        highest_level: the maximum logging level in use.
    -            This would only need to be changed if a custom level greater than CRITICAL is defined.
    -        flag: True to disable all logging [default=True]
    +        highest_level (int, optional): The maximum logging level to be disabled.
    +            Defaults to logging.CRITICAL.
    +            Note: Adjust this only if a custom level greater than CRITICAL is defined.
    +        flag (bool, optional): If True, disables all logging; if False, no changes are made.
    +            Defaults to True.
    +
    +    Example:
    +        >>> with all_logging_disabled():
    +        ...     # Your code with logging messages disabled
    +
    +    Note:
    +        This context manager is designed to prevent any logging messages triggered during its body
    +        from being processed. It temporarily disables logging and restores the previous state afterward.
         """
    +
         # two kind-of hacks here:
         #    * can't get the highest logging level in effect => delegate to the user
         #    * can't get the current module-level override => use an undocumented
    @@ -1426,8 +1772,7 @@ 

    Raises

    Expand source code -
    def check_argument_type(
    -        obj: object, name: str, target_class: Union[type, Tuple[type]], allow_none: bool = False):
    +
    def check_argument_type(obj: object, name: str, target_class: Union[type, Tuple[type]], allow_none: bool = False):
         """Check that the given object is of a specific (sub)type of the given target_class.
     
         The target_class can be a tuple of types.
    @@ -1447,13 +1792,51 @@ 

    Raises

    def check_is_a_string(var, allow_none=False)
    -

    Calls is_a_string and raises a type error if the check fails.

    +

    Checks if the given variable is a string and raises a TypeError if the check fails.

    +

    Args

    +
    +
    var
    +
    The variable to be checked.
    +
    allow_none : bool, optional
    +
    If True, allows the variable to be None without raising an error. +Defaults to False.
    +
    +

    Raises

    +
    +
    TypeError
    +
    If the variable is not a string or is None (when allow_none is False).
    +
    +

    Example

    +
    >>> check_is_a_string("example")
    +
    +

    Note

    +

    This function is designed to validate that the input variable is a string. +If allow_none is set to True, it allows the variable to be None without raising an error.

    Expand source code
    def check_is_a_string(var, allow_none=False):
    -    """ Calls is_a_string and raises a type error if the check fails."""
    +    """
    +    Checks if the given variable is a string and raises a TypeError if the check fails.
    +
    +    Args:
    +        var: The variable to be checked.
    +        allow_none (bool, optional): If True, allows the variable to be None without raising an error.
    +            Defaults to False.
    +
    +    Raises:
    +        TypeError: If the variable is not a string or is None (when allow_none is False).
    +
    +    Example:
    +        >>> check_is_a_string("example")
    +
    +    Note:
    +        This function is designed to validate that the input variable is a string.
    +        If `allow_none` is set to True, it allows the variable to be None without raising an error.
    +
    +    """
    +
         if var is None and allow_none:
             return
         if var is None and not allow_none:
    @@ -1474,12 +1857,26 @@ 

    Raises

    def check_str_for_slash(arg: str):
         """Check if there is a slash in the given string, and raise a ValueError if so."""
     
    -    if '/' in arg:
    +    if "/" in arg:
             ValueError(f"The given argument can not contain slashes, {arg=}.")
    +
    +def clear_average_execution_times() +
    +
    +

    Clear out all function timing for this process.

    +
    + +Expand source code + +
    def clear_average_execution_times():
    +    """Clear out all function timing for this process."""
    +    _function_timing.clear()
    +
    +
    -def do_every(period: float, func: , *args) ‑> None +def do_every(period: float, func: callable, *args) ‑> None

    This method executes a function periodically, taking into account @@ -1543,6 +1940,55 @@

    Args

    func(*args)
    +
    +def duration(dt_start: str | datetime.datetime, dt_end: str | datetime.datetime) ‑> datetime.timedelta +
    +
    +

    Returns a timedelta object with the duration, i.e. time difference between dt_start and dt_end.

    +

    Notes

    +

    If you need the number of seconds of your measurement, use the total_seconds() method of +the timedelta object.

    +

    Even if you —by accident— switch the start and end time arguments, the duration will +be calculated as expected.

    +

    Args

    +
    +
    dt_start
    +
    start time of the measurement
    +
    dt_end
    +
    end time of the measurement
    +
    +

    Returns

    +

    The time difference (duration) between dt_start and dt_end.

    +
    + +Expand source code + +
    def duration(dt_start: str | datetime.datetime, dt_end: str | datetime.datetime) -> datetime.timedelta:
    +    """
    +    Returns a `timedelta` object with the duration, i.e. time difference between dt_start and dt_end.
    +
    +    Notes:
    +        If you need the number of seconds of your measurement, use the `total_seconds()` method of
    +        the timedelta object.
    +
    +        Even if you —by accident— switch the start and end time arguments, the duration will
    +        be calculated as expected.
    +
    +    Args:
    +        dt_start: start time of the measurement
    +        dt_end: end time of the measurement
    +
    +    Returns:
    +        The time difference (duration) between dt_start and dt_end.
    +    """
    +    if isinstance(dt_start, str):
    +        dt_start = str_to_datetime(dt_start)
    +    if isinstance(dt_end, str):
    +        dt_end = str_to_datetime(dt_end)
    +
    +    return dt_end - dt_start if dt_end > dt_start else dt_start - dt_end
    +
    +
    def env_var(**kwargs)
    @@ -1591,8 +2037,38 @@

    Examples

    del os.environ[k]
    -
    -def filter_by_attr(elements: Iterable[+T_co], **attrs) ‑> List[~T] +
    +def execution_time(func) +
    +
    +

    A decorator to save the execution time of the function. Use this decorator +if you want —by default and always— have an idea of the average execution time +of the given function.

    +

    Use this in conjunction with the get_average_execution_time() function to +retrieve the average execution time for the given function.

    +
    + +Expand source code + +
    def execution_time(func):
    +    """
    +    A decorator to save the execution time of the function. Use this decorator
    +    if you want —by default and always— have an idea of the average execution time
    +    of the given function.
    +
    +    Use this in conjunction with the `get_average_execution_time()` function to
    +    retrieve the average execution time for the given function.
    +    """
    +
    +    @functools.wraps(func)
    +    def wrapper(*args, **kwargs):
    +        return save_average_execution_time(func, *args, **kwargs)
    +
    +    return wrapper
    +
    +
    +
    +def filter_by_attr(elements: Iterable, **attrs) ‑> List[~T]

    A helper that returns the elements from the iterable that meet all the traits passed in attrs.

    @@ -1689,11 +2165,7 @@

    Args

    value = (operator.eq, value) attr_func_values.append((attr.replace("__", "."), *value)) - return [ - el - for el in elements - if all(check(attr, func, value, el) for attr, func, value in attr_func_values) - ]
    + return [el for el in elements if all(check(attr, func, value, el) for attr, func, value in attr_func_values)]
    @@ -1847,7 +2319,7 @@

    Raises

    Expand source code -
    def format_datetime(dt: Union[str,datetime.datetime] = None, fmt: str = None, width: int = 6, precision: int = 3):
    +
    def format_datetime(dt: Union[str, datetime.datetime] = None, fmt: str = None, width: int = 6, precision: int = 3):
         """Format a datetime as YYYY-mm-ddTHH:MM:SS.μs+0000.
     
         If the given argument is not timezone aware, the last part, i.e. `+0000` will not be there.
    @@ -1918,15 +2390,39 @@ 

    Raises

    def get_active_loggers() ‑> dict
    -
    +

    Retrieves information about active loggers and their respective log levels.

    +

    Returns a dictionary where keys are the names of active loggers, and values +are the corresponding log levels in string format.

    +

    Returns

    +
    +
    dict
    +
    A dictionary mapping logger names to their log levels.
    +
    +

    Note

    +

    This function provides a snapshot of the currently active loggers and +their log levels at the time of the function call.

    Expand source code
    def get_active_loggers() -> dict:
    +    """
    +    Retrieves information about active loggers and their respective log levels.
    +
    +    Returns a dictionary where keys are the names of active loggers, and values
    +    are the corresponding log levels in string format.
    +
    +    Returns:
    +        dict: A dictionary mapping logger names to their log levels.
    +
    +    Note:
    +        This function provides a snapshot of the currently active loggers and
    +        their log levels at the time of the function call.
    +
    +    """
    +
         return {
    -        name: logging.getLevelName(logging.getLogger(name).level)
    -        for name in sorted(logging.Logger.manager.loggerDict)
    +        name: logging.getLevelName(logging.getLogger(name).level) for name in sorted(logging.Logger.manager.loggerDict)
         }
    @@ -1936,6 +2432,7 @@

    Raises

    Returns the average execution time of the given function. The function 'func' shall be previously executed using the save_average_execution_time() function which remembers the last 100 execution times of the function. +You can also decorate your function with @execution_time to permanently monitor it. The average time is a moving average over the last 100 times. If the function was never called before, 0.0 is returned.

    This function can be used when setting a frequency to execute a certain function. When the average execution time @@ -1949,6 +2446,7 @@

    Raises

    """ Returns the average execution time of the given function. The function 'func' shall be previously executed using the `save_average_execution_time()` function which remembers the last 100 execution times of the function. + You can also decorate your function with `@execution_time` to permanently monitor it. The average time is a moving average over the last 100 times. If the function was never called before, 0.0 is returned. @@ -1957,6 +2455,12 @@

    Raises

    stalled. """ + # If the function was previously wrapped with the `@execution_time` wrapper, we need to get + # to the original function object because that's the one that is saved. + + with contextlib.suppress(AttributeError): + func = func.__wrapped__ + try: d = _function_timing[func] return sum(d) / len(d) @@ -1964,8 +2468,26 @@

    Raises

    return 0.0
    +
    +def get_average_execution_times() ‑> dict +
    +
    +

    Returns a dictionary with "function name": average execution time, for all function that have been +monitored in this process.

    +
    + +Expand source code + +
    def get_average_execution_times() -> dict:
    +    """
    +    Returns a dictionary with "function name": average execution time, for all function that have been
    +    monitored in this process.
    +    """
    +    return {func.__name__: get_average_execution_time(func) for func in _function_timing}
    +
    +
    -def get_caller_info(level=1) ‑>  +def get_caller_info(level=1) ‑> CallerInfo

    Returns the filename, function name and lineno of the caller.

    @@ -1988,7 +2510,7 @@

    Returns

    Expand source code -
    def get_caller_info(level=1) -> NamedTuple:
    +
    def get_caller_info(level=1) -> CallerInfo:
         """
         Returns the filename, function name and lineno of the caller.
     
    @@ -2011,25 +2533,60 @@ 

    Returns

    frame = frame.f_back frame_info = inspect.getframeinfo(frame) - caller_info = namedtuple("CallerInfo", "filename function lineno") - - return caller_info(frame_info.filename, frame_info.function, frame_info.lineno)
    + return CallerInfo(frame_info.filename, frame_info.function, frame_info.lineno)
    def get_full_classname(obj: object) ‑> str
    -

    Returns the fully qualified class name for this object.

    +

    Returns the fully qualified class name for the given object.

    +

    Args

    +
    +
    obj : object
    +
    The object for which to retrieve the fully qualified class name.
    +
    +

    Returns

    +
    +
    str
    +
    The fully qualified class name, including the module.
    +
    +

    Example

    +
    >>> get_full_classname("example")
    +'builtins.str'
    +
    +
    >>> get_full_classname(42)
    +'builtins.int'
    +
    +

    Note

    +

    The function considers various scenarios, such as objects being classes, +built-ins, or literals like int, float, or complex numbers.

    Expand source code
    def get_full_classname(obj: object) -> str:
    -    """Returns the fully qualified class name for this object."""
    +    """
    +    Returns the fully qualified class name for the given object.
    +
    +    Args:
    +        obj (object): The object for which to retrieve the fully qualified class name.
    +
    +    Returns:
    +        str: The fully qualified class name, including the module.
    +
    +    Example:
    +        >>> get_full_classname("example")
    +        'builtins.str'
    +
    +        >>> get_full_classname(42)
    +        'builtins.int'
    +
    +    Note:
    +        The function considers various scenarios, such as objects being classes,
    +        built-ins, or literals like int, float, or complex numbers.
     
    -    # Take into account that obj might be a class or a builtin or even a
    -    # literal like an int or a float or a complex number
    +    """
     
         if type(obj) is type or obj.__class__.__module__ == str.__module__:
             try:
    @@ -2042,7 +2599,7 @@ 

    Returns

    module = obj.__class__.__module__ name = obj.__class__.__qualname__ - return module + '.' + name
    + return module + "." + name
    @@ -2063,8 +2620,8 @@

    Returns

    try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - #sock.connect(("8.8.8.8", 80)) - sock.connect(('10.255.255.255', 1)) + # sock.connect(("8.8.8.8", 80)) + sock.connect(("10.255.255.255", 1)) host_ip = sock.getsockname()[0] sock.close() except Exception as exc: @@ -2078,10 +2635,83 @@

    Returns

    host_name = socket.gethostname() host_ip = socket.gethostbyname(host_name) return host_ip - except (Exception, ): + except (Exception,): return None
    +
    +def get_module_location(arg) ‑> Optional[pathlib.Path] +
    +
    +

    Returns the location of the module as a Path object.

    +

    The function accepts a string (module name), a function, or a module. +For functions and modules, the module name is determined internally.

    +

    Args

    +
    +
    arg : Union[FunctionType, ModuleType, str]
    +
    The function, module, or module name.
    +
    +

    Returns

    +
    +
    Optional[Path]
    +
    The location of the module as a Path object, or None if the location
    +
    +

    cannot be determined or an invalid argument is provided.

    +

    Example

    +
    >>> get_module_location('egse')
    +Path('/path/to/egse')
    +
    +
    >>> get_module_location(egse.system)
    +Path('/path/to/egse/system')
    +
    +

    Note

    +

    If the module is not found or is not a valid module, None is returned.

    +
    + +Expand source code + +
    def get_module_location(arg) -> Optional[Path]:
    +    """
    +    Returns the location of the module as a Path object.
    +
    +    The function accepts a string (module name), a function, or a module.
    +    For functions and modules, the module name is determined internally.
    +
    +    Args:
    +        arg (Union[FunctionType, ModuleType, str]): The function, module, or module name.
    +
    +    Returns:
    +        Optional[Path]: The location of the module as a Path object, or None if the location
    +        cannot be determined or an invalid argument is provided.
    +
    +    Example:
    +        >>> get_module_location('egse')
    +        Path('/path/to/egse')
    +
    +        >>> get_module_location(egse.system)
    +        Path('/path/to/egse/system')
    +
    +    Note:
    +        If the module is not found or is not a valid module, None is returned.
    +
    +    """
    +    if isinstance(arg, FunctionType):
    +        module_name = arg.__module__
    +    elif isinstance(arg, ModuleType):
    +        module_name = arg.__name__
    +    elif isinstance(arg, str):
    +        module_name = arg
    +    else:
    +        return None
    +
    +    try:
    +        module = importlib.import_module(module_name)
    +    except TypeError as exc:
    +        return None
    +
    +    return Path(module.__file__).parent.resolve()
    +
    +
    def get_os_name() ‑> str
    @@ -2157,6 +2787,77 @@

    Returns

    return "unknown"
    +
    +def get_package_location(module) ‑> List[pathlib.Path] +
    +
    +

    Retrieves the file system locations associated with a Python package.

    +

    This function takes a module, module name, or fully qualified module path, +and returns a list of Path objects representing the file system locations +associated with the package. If the module is a namespace package, it returns +the paths of all namespaces; otherwise, it returns the location of the module.

    +

    Args

    +
    +
    module : Union[FunctionType, ModuleType, str]
    +
    The module or module name to +retrieve locations for.
    +
    +

    Returns

    +
    +
    List[Path]
    +
    A list of Path objects representing the file system locations.
    +
    +

    Note

    +

    If the module is not found or is not a valid module, an empty list is returned.

    +
    + +Expand source code + +
    def get_package_location(module) -> List[Path]:
    +    """
    +    Retrieves the file system locations associated with a Python package.
    +
    +    This function takes a module, module name, or fully qualified module path,
    +    and returns a list of Path objects representing the file system locations
    +    associated with the package. If the module is a namespace package, it returns
    +    the paths of all namespaces; otherwise, it returns the location of the module.
    +
    +    Args:
    +        module (Union[FunctionType, ModuleType, str]): The module or module name to
    +            retrieve locations for.
    +
    +    Returns:
    +        List[Path]: A list of Path objects representing the file system locations.
    +
    +    Note:
    +        If the module is not found or is not a valid module, an empty list is returned.
    +
    +    """
    +
    +    if isinstance(module, FunctionType):
    +        module_name = module.__module__
    +    elif isinstance(module, ModuleType):
    +        module_name = module.__name__
    +    elif isinstance(module, str):
    +        module_name = module
    +    else:
    +        return []
    +
    +    try:
    +        module = importlib.import_module(module)
    +    except TypeError as exc:
    +        return []
    +
    +    if is_namespace(module):
    +        return [
    +            Path(location)
    +            for location in module.__path__
    +        ]
    +    else:
    +        location = get_module_location(module)
    +        return [] if location is None else [location]
    +
    +
    def get_referenced_var_name(obj: Any) ‑> List[str]
    @@ -2194,6 +2895,26 @@

    Returns

    return name_set or []
    +
    +def get_system_architecture() ‑> str +
    +
    +

    Returns the machine type. This is a string describing the processor architecture, +like i386 or arm64, but the exact string is not defined. An empty string can be returned when +the type cannot be determined.

    +
    + +Expand source code + +
    def get_system_architecture() -> str:
    +    """
    +    Returns the machine type. This is a string describing the processor architecture,
    +    like i386 or arm64, but the exact string is not defined. An empty string can be returned when
    +    the type cannot be determined.
    +    """
    +    return platform.machine()
    +
    +
    def get_system_name() ‑> str
    @@ -2290,9 +3011,9 @@

    Returns

    boot_time = psutil.boot_time() statistics["boot_time"] = boot_time - statistics["since"] = datetime.datetime.fromtimestamp( - boot_time, tz=datetime.timezone.utc - ).strftime("%Y-%m-%d %H:%M:%S") + statistics["since"] = datetime.datetime.fromtimestamp(boot_time, tz=datetime.timezone.utc).strftime( + "%Y-%m-%d %H:%M:%S" + ) return statistics
    @@ -2350,7 +3071,7 @@

    Returns

    -def humanize_seconds(seconds: float) +def humanize_seconds(seconds: float, include_micro_seconds: bool = True)

    The number of seconds is represented as "[#D]d [#H]h[#M]m[#S]s.MS" where:

    @@ -2368,6 +3089,8 @@

    Examples

    '10d 00s.000' >>> humanize_seconds(10*86400 + 3*3600 + 42.023) '10d 03h00m42s.023' +>>> humanize_seconds(10*86400 + 3*3600 + 42.023, include_micro_seconds=False) +'10d 03h00m42s'

    Returns

    a string representation for the number of seconds.

    @@ -2375,7 +3098,7 @@

    Returns

    Expand source code -
    def humanize_seconds(seconds: float):
    +
    def humanize_seconds(seconds: float, include_micro_seconds: bool = True):
         """
         The number of seconds is represented as "[#D]d [#H]h[#M]m[#S]s.MS" where:
     
    @@ -2392,6 +3115,8 @@ 

    Returns

    '10d 00s.000' >>> humanize_seconds(10*86400 + 3*3600 + 42.023) '10d 03h00m42s.023' + >>> humanize_seconds(10*86400 + 3*3600 + 42.023, include_micro_seconds=False) + '10d 03h00m42s' Returns: a string representation for the number of seconds. @@ -2421,7 +3146,8 @@

    Returns

    result += f"{minutes:02d}m" result += f"{seconds:02d}s" - result += f".{micro_seconds:03d}" + if include_micro_seconds: + result += f".{micro_seconds:03d}" return result
    @@ -2432,7 +3158,7 @@

    Returns

    Ignore RuntimeWarning by runpy that occurs when executing a module with python -m package.module, while that module is also imported.

    -

    The original warning mssage is:

    +

    The original warning message is:

    '<package.module>' found in sys.modules after import of package '<package'>,
     but prior to execution of '<package.module>'
     
    @@ -2445,7 +3171,7 @@

    Returns

    Ignore RuntimeWarning by `runpy` that occurs when executing a module with `python -m package.module`, while that module is also imported. - The original warning mssage is: + The original warning message is: '<package.module>' found in sys.modules after import of package '<package'>, but prior to execution of '<package.module>' @@ -2460,7 +3186,7 @@

    Returns

    msg = "'{module}' found in sys.modules after import of package" for module in modules: module_msg = re.escape(msg.format(module=module)) - warnings.filterwarnings("ignore", message=module_msg, category=RuntimeWarning, module='runpy') # ignore -m + warnings.filterwarnings("ignore", message=module_msg, category=RuntimeWarning, module="runpy") # ignore -m except (ImportError, KeyError, AttributeError, Exception): pass
    @@ -2493,6 +3219,57 @@

    Returns

    return hasattr(builtins, "__IPYTHON__")
    +
    +def is_namespace(module) ‑> bool +
    +
    +

    Checks if a module represents a namespace package.

    +

    A namespace package is defined as a module that has a 'path' attribute +and no 'file' attribute.

    +

    Args

    +
    +
    module
    +
    The module to be checked.
    +
    +

    Returns

    +
    +
    bool
    +
    True if the module is a namespace package, False otherwise.
    +
    +

    Note

    +

    A namespace package is a special kind of package that does not contain +an actual implementation but serves as a container for other packages +or modules.

    +
    + +Expand source code + +
    def is_namespace(module) -> bool:
    +    """
    +    Checks if a module represents a namespace package.
    +
    +    A namespace package is defined as a module that has a '__path__' attribute
    +    and no '__file__' attribute.
    +
    +    Args:
    +        module: The module to be checked.
    +
    +    Returns:
    +        bool: True if the module is a namespace package, False otherwise.
    +
    +    Note:
    +        A namespace package is a special kind of package that does not contain
    +        an actual implementation but serves as a container for other packages
    +        or modules.
    +
    +    """
    +
    +    if hasattr(module, '__path__') and getattr(module, '__file__', None) is None:
    +        return True
    +    else:
    +        return False
    +
    +
    def is_not_in(a, b)
    @@ -2507,6 +3284,23 @@

    Returns

    return a not in b
    +
    +def now(utc: bool = True) +
    +
    +

    Returns a datetime object for the current time in UTC or local time.

    +
    + +Expand source code + +
    def now(utc: bool = True):
    +    """Returns a datetime object for the current time in UTC or local time."""
    +    if utc:
    +        return datetime.datetime.now(tz=datetime.timezone.utc)
    +    else:
    +        return datetime.datetime.now()
    +
    +
    def ping(host, timeout: float = 3.0)
    @@ -2595,7 +3389,6 @@

    Returns

    filename = Path(filename) if not filename.exists(): - return None with filename.open("rb") as file: @@ -2624,7 +3417,7 @@

    Args

    Expand source code
    def read_last_lines(filename: str, num_lines: int):
    -    """ Return the last lines of a text file.
    +    """Return the last lines of a text file.
     
         Args:
             - filename: Filename.
    @@ -2641,7 +3434,6 @@ 

    Args

    assert num_lines > 1 if not filename.exists(): - return None assert num_lines >= 0 @@ -2655,20 +3447,15 @@

    Args

    lines = [] with open(filename) as f: - while len(lines) <= num_lines: - try: - f.seek(-pos, 2) except IOError: - f.seek(0) break finally: - lines = list(f) # Increasing value of variable exponentially @@ -2809,17 +3596,54 @@

    Returns

    def sanity_check(flag: bool, msg: str)
    -

    This is a replacement for the 'assert' statement. Use this in production code -such that your checks are not removed during optimisations.

    +

    Checks a boolean flag and raises an AssertionError with the provided message if the check fails.

    +

    This function serves as a replacement for the 'assert' statement in production code. +Using this ensures that your checks are not removed during optimizations.

    +

    Args

    +
    +
    flag : bool
    +
    The boolean flag to be checked.
    +
    msg : str
    +
    The message to be included in the AssertionError if the check fails.
    +
    +

    Raises

    +
    +
    AssertionError
    +
    If the flag is False.
    +
    +

    Example

    +
    >>> sanity_check(x > 0, "x must be greater than 0")
    +
    +

    Note

    +

    This function is designed for production code to perform runtime checks +that won't be removed during optimizations.

    Expand source code
    def sanity_check(flag: bool, msg: str):
         """
    -    This is a replacement for the 'assert' statement. Use this in production code
    -    such that your checks are not removed during optimisations.
    +    Checks a boolean flag and raises an AssertionError with the provided message if the check fails.
    +
    +    This function serves as a replacement for the 'assert' statement in production code.
    +    Using this ensures that your checks are not removed during optimizations.
    +
    +    Args:
    +        flag (bool): The boolean flag to be checked.
    +        msg (str): The message to be included in the AssertionError if the check fails.
    +
    +    Raises:
    +        AssertionError: If the flag is False.
    +
    +    Example:
    +        >>> sanity_check(x > 0, "x must be greater than 0")
    +
    +    Note:
    +        This function is designed for production code to perform runtime checks
    +        that won't be removed during optimizations.
    +
         """
    +
         if not flag:
             raise AssertionError(msg)
    @@ -2870,7 +3694,7 @@

    Args

    Expand source code
    def str_to_datetime(datetime_string: str):
    -    """ Convert the given string to a datetime object.
    +    """Convert the given string to a datetime object.
     
         Args:
             - datatime_string: String representing a datetime, in the format %Y-%m-%dT%H:%M:%S.%f%z.
    @@ -2896,7 +3720,7 @@ 

    Args

    Expand source code
    def time_since_epoch_1958(datetime_string: str):
    -    """ Calculate the time since epoch 1958 for the given string representation of a datetime.
    +    """Calculate the time since epoch 1958 for the given string representation of a datetime.
     
         Args:
             - datetime_string: String representing a datetime, in the format %Y-%m-%dT%H:%M:%S.%f%z.
    @@ -2904,7 +3728,7 @@ 

    Args

    Returns: Time since the 1958 epoch [s]. """ - time_since_epoch_1970 = str_to_datetime(datetime_string).timestamp() # Since Jan 1st, 1970, midnight + time_since_epoch_1970 = str_to_datetime(datetime_string).timestamp() # Since Jan 1st, 1970, midnight return time_since_epoch_1970 + EPOCH_1958_1970
    @@ -2919,7 +3743,7 @@

    Args

    Expand source code
    def type_name(var):
    -    """ Returns the name of the type of var."""
    +    """Returns the name of the type of var."""
         return type(var).__name__
    @@ -3020,8 +3844,106 @@

    Returns

    return False
    +
    +def waiting_for(condition, *args, interval=0.1, timeout=1, verbose=False, **kwargs) +
    +
    +

    Sleep until the given condition is fulfilled. The arguments are passed into the condition +callable which is called in a while loop until the condition is met or the timeout is reached.

    +

    Note that the condition can be a function, method or callable class object. +An example of the latter is:

    +
    class SleepUntilCount:
    +    def __init__(self, end):
    +        self._end = end
    +        self._count = 0
    +
    +    def __call__(self, *args, **kwargs):
    +        self._count += 1
    +        if self._count >= self._end:
    +            return True
    +        else:
    +            return False
    +
    +

    Args

    +
    +
    condition
    +
    a callable that returns True when the condition is met, False otherwise
    +
    interval
    +
    the sleep interval between condition checks [s, default=0.1]
    +
    timeout
    +
    the period after which the function returns, even when the condition is +not met [s, default=1]
    +
    verbose
    +
    log debugging messages if True
    +
    *args
    +
    any arguments that will be passed into the condition function
    +
    +

    Raises

    +

    A TimeoutError when the condition was not fulfilled within the timeout period.

    +
    + +Expand source code + +
    def waiting_for(condition, *args, interval=0.1, timeout=1, verbose=False, **kwargs):
    +    """
    +    Sleep until the given condition is fulfilled. The arguments are passed into the condition
    +    callable which is called in a while loop until the condition is met or the timeout is reached.
    +
    +    Note that the condition can be a function, method or callable class object.
    +    An example of the latter is:
    +
    +        class SleepUntilCount:
    +            def __init__(self, end):
    +                self._end = end
    +                self._count = 0
    +
    +            def __call__(self, *args, **kwargs):
    +                self._count += 1
    +                if self._count >= self._end:
    +                    return True
    +                else:
    +                    return False
    +
    +
    +    Args:
    +        condition: a callable that returns True when the condition is met, False otherwise
    +        interval: the sleep interval between condition checks [s, default=0.1]
    +        timeout: the period after which the function returns, even when the condition is
    +            not met [s, default=1]
    +        verbose: log debugging messages if True
    +        *args: any arguments that will be passed into the condition function
    +
    +    Raises:
    +        A TimeoutError when the condition was not fulfilled within the timeout period.
    +    """
    +
    +    if inspect.isfunction(condition) or inspect.ismethod(condition):
    +        func_name = condition.__name__
    +    else:
    +        func_name = condition.__class__.__name__
    +
    +    caller = get_caller_info(level=2)
    +
    +    start = time.time()
    +
    +    while not condition(*args, **kwargs):
    +        if time.time() - start > timeout:
    +            raise TimeoutError(
    +                f"Timeout after {timeout} sec, from {caller.filename} at {caller.lineno},"
    +                f" {func_name}{args} not met."
    +            )
    +        time.sleep(interval)
    +
    +    duration = time.time() - start
    +
    +    if verbose:
    +        logger.debug(f"waiting_for finished successfully after {duration:.3f}s, {func_name}{args}{kwargs} is met.")
    +
    +    return duration
    +
    +
    -def walk_dict_tree(dictionary: dict, tree: rich.tree.Tree, text_style: str = 'green') +def walk_dict_tree(dictionary: dict, tree: Tree, text_style: str = 'green')
    @@ -3104,7 +4026,6 @@

    Classes

    return tree def __repr__(self): - # We only want the first 10 key:value pairs count = 10 @@ -3119,6 +4040,32 @@

    Ancestors

  • builtins.dict
  • +
    +class CallerInfo +(filename, function, lineno) +
    +
    +

    CallerInfo(filename, function, lineno)

    +

    Ancestors

    +
      +
    • builtins.tuple
    • +
    +

    Instance variables

    +
    +
    var filename
    +
    +

    Alias for field number 0

    +
    +
    var function
    +
    +

    Alias for field number 1

    +
    +
    var lineno
    +
    +

    Alias for field number 2

    +
    +
    +
    class NotSpecified
    @@ -3131,19 +4078,18 @@

    Ancestors

    Expand source code
    class NotSpecified:
    -    """ Class for NOT_SPECIFIED constant.
    -        Is used so that a parameter can have a default value other than None.
    +    """Class for NOT_SPECIFIED constant.
    +    Is used so that a parameter can have a default value other than None.
     
    -        Evaluate to False when converted to boolean.
    +    Evaluate to False when converted to boolean.
         """
    +
         def __nonzero__(self):
    -        """ Always returns False. Called when to converting to bool in Python 2.
    -        """
    +        """Always returns False. Called when to converting to bool in Python 2."""
             return False
     
         def __bool__(self):
    -        """ Always returns False. Called when to converting to bool in Python 3.
    -        """
    +        """Always returns False. Called when to converting to bool in Python 3."""
             return False
    @@ -3362,6 +4308,10 @@

    Returns

    self.name = name self.precision = precision self.log_level = log_level + caller_info = get_caller_info(level=2) + self.filename = caller_info.filename + self.func = caller_info.function + self.lineno = caller_info.lineno def __enter__(self): # start is a value containing the start time in fractional seconds @@ -3377,8 +4327,8 @@

    Returns

    # Overwrite self.end() so that it always returns the fixed end time self.end = self._end - logger.log(self.log_level, - f"{self.name}: {self.end() - self.start:0.{self.precision}f} seconds") + logger.log(self.log_level, f"{self.name} [{self.filename}:{self.func}:{self.lineno}]: " + f"{self.end() - self.start:0.{self.precision}f} seconds") return False def __call__(self): @@ -3386,8 +4336,8 @@

    Returns

    def log_elapsed(self): """Sends the elapsed time info to the default logger.""" - logger.log(self.log_level, - f"{self.name}: {self.end() - self.start:0.{self.precision}f} seconds elapsed") + logger.log(self.log_level, f"{self.name} [{self.func}:{self.lineno}]: " + f"{self.end() - self.start:0.{self.precision}f} seconds elapsed") def get_elapsed(self) -> float: """Returns the elapsed time for this timer as a float in seconds.""" @@ -3423,8 +4373,8 @@

    Methods

    def log_elapsed(self):
         """Sends the elapsed time info to the default logger."""
    -    logger.log(self.log_level,
    -               f"{self.name}: {self.end() - self.start:0.{self.precision}f} seconds elapsed")
    + logger.log(self.log_level, f"{self.name} [{self.func}:{self.lineno}]: " + f"{self.end() - self.start:0.{self.precision}f} seconds elapsed")
    @@ -3450,20 +4400,27 @@

    Index

  • check_argument_type
  • check_is_a_string
  • check_str_for_slash
  • +
  • clear_average_execution_times
  • do_every
  • +
  • duration
  • env_var
  • +
  • execution_time
  • filter_by_attr
  • find_class
  • flatten_dict
  • format_datetime
  • get_active_loggers
  • get_average_execution_time
  • +
  • get_average_execution_times
  • get_caller_info
  • get_full_classname
  • get_host_ip
  • +
  • get_module_location
  • get_os_name
  • get_os_version
  • +
  • get_package_location
  • get_referenced_var_name
  • +
  • get_system_architecture
  • get_system_name
  • get_system_stats
  • has_internet
  • @@ -3471,7 +4428,9 @@

    Index

  • ignore_m_warning
  • is_in
  • is_in_ipython
  • +
  • is_namespace
  • is_not_in
  • +
  • now
  • ping
  • read_last_line
  • read_last_lines
  • @@ -3483,6 +4442,7 @@

    Index

  • time_since_epoch_1958
  • type_name
  • wait_until
  • +
  • waiting_for
  • walk_dict_tree
  • @@ -3492,6 +4452,14 @@

    Index

    AttributeDict

  • +

    CallerInfo

    + +
  • +
  • NotSpecified

  • diff --git a/docs/api/egse/tcs/tcs.html b/docs/api/egse/tcs/tcs.html index bceb732..6fa19f0 100644 --- a/docs/api/egse/tcs/tcs.html +++ b/docs/api/egse/tcs/tcs.html @@ -60,6 +60,7 @@

    Module egse.tcs.tcs

    from egse.mixin import dynamic_command from egse.proxy import DynamicProxy from egse.settings import Settings +from egse.setup import load_setup, Setup from egse.sockets import SocketInterface from egse.storage import StorageProxy from egse.storage import register_to_storage_manager @@ -152,12 +153,12 @@

    Module egse.tcs.tcs

    return data -def process_all_housekeeping(data: bytes) -> Union[List[List], Failure]: +def process_all_housekeeping(response: bytes) -> Union[List[List], Failure]: """ Process the response from the TCS EGSE remote command `get_TM_All`. Args: - data (str): a string containing the response from the TCS EGSE to the get_TM_All command. + response (bytes): contains the response from the TCS EGSE to the get_TM_All command. Returns: A nested list where the inner lists contain the name, timestamp and value of a housekeeping @@ -167,7 +168,7 @@

    Module egse.tcs.tcs

    communication error occurred. """ - data = data.decode().split('\x03') + data = response.decode().split('\x03') if "not_allowed_remote_mode_not_active" in data: msg = "Remote Control not active, no housekeeping values received." @@ -185,26 +186,26 @@

    Module egse.tcs.tcs

    return data -def process_get_housekeeping_value(data: bytes) -> Union[HousekeepingValue, Failure]: +def process_get_housekeeping_value(response: bytes) -> Union[HousekeepingValue, Failure]: - if b"remote_mode_not_active" in data: + if b"remote_mode_not_active" in response: logger.warning("Requesting housekeeping value invalid, not in remote control mode.") return Failure("Requesting housekeeping value invalid, not in remote control mode.") - if b"invalid_tm_item_id" in data: + if b"invalid_tm_item_id" in response: logger.warning("Invalid housekeeping parameter name.") return Failure("Invalid housekeeping parameter name.") - data = data.decode().split('\x03') + data = response.decode().split('\x03') data = data[0].split('\t') return HousekeepingValue(*data) -def process_configuration(tcs_conf: bytes) -> Union[Dict, Failure]: +def process_configuration(response: bytes) -> Union[Dict, Failure]: """ Process the response to the get_configuration command. Processing in this context means to disentangle the returned string and split it up in parameter name, value pairs. Args: - tcs_conf (str): a string containing the response from the TCS EGSE + response (str): a string containing the response from the TCS EGSE Returns: A dictionary with the parameters names and their current values. @@ -213,7 +214,7 @@

    Module egse.tcs.tcs

    # The rest of this function parse and organise the configuration parameters # in a list of lists with [[name, value], [...]] - tcs_conf = tcs_conf.decode().split('\x03') + tcs_conf = response.decode().split('\x03') # Remove empty items from the list. The list ends always with an empty item because # the response ends with the '\x03' EOT character. @@ -592,12 +593,13 @@

    Module egse.tcs.tcs

    class TCSParameterNaming: """Defines the mapping between TCS EGSE device names and the CGSE names.""" - def __init__(self, origin: str): + def __init__(self, origin: str, setup: Setup): + # The hk_names_mapping is a dictionary that maps the original device telemetry parameter # names to the correct device names as defined in the CGSE. The keys in the mapping are # the original device name, the values are the CGSE corrected names. - self.hk_names_mapping = read_conversion_dict(storage_mnemonic=origin, use_site=False) + self.hk_names_mapping = read_conversion_dict(storage_mnemonic=origin, use_site=False, setup=setup) def get_all_cgse_names(self): """Returns the correct CGSE housekeeping parameter names.""" @@ -613,9 +615,10 @@

    Module egse.tcs.tcs

    class TCSTelemetryMetrics: """Defines the metrics for the TCS EGSE that are maintained by the TCSTelemetry class.""" - def __init__(self): - self.hk_metrics = define_metrics(origin=ORIGIN, dashboard="TCS_TRP_MON") - self.hk_metrics_status = define_metrics(origin=ORIGIN, dashboard="TCS_STATUS_MON") + def __init__(self, setup: Setup): + + self.hk_metrics = define_metrics(origin=ORIGIN, dashboard="TCS_TRP_MON", setup=setup) + self.hk_metrics_status = define_metrics(origin=ORIGIN, dashboard="TCS_STATUS_MON", setup=setup) def update_metrics(self, name: str, value: str): """Update the TCS metric parameter with the given value.""" @@ -640,6 +643,7 @@

    Module egse.tcs.tcs

    self._killer = None self._metrics: TCSTelemetryMetrics = None self._hk_names: TCSParameterNaming = None + self.setup = load_setup() def run(self): @@ -652,15 +656,16 @@

    Module egse.tcs.tcs

    self._killer = SignalCatcher() + # Device naming is different from CGSE naming, the conversion is handled in the # TCSParameterNaming class. - self._hk_names = TCSParameterNaming(origin=ORIGIN) + self._hk_names = TCSParameterNaming(origin=ORIGIN, setup=self.setup) # Metrics must be defined only when the sub-process already exists, otherwise a TypeError # will be raised, indicating that a thread lock cannot be pickled - self._metrics = TCSTelemetryMetrics() + self._metrics = TCSTelemetryMetrics(self.setup) # These are only 61 of the telemetry parameters of the TCS EGSE. It is not possible to know # all the parameters beforehand since they are only published when changed. So, eventually, @@ -1089,14 +1094,14 @@

    Returns

    -def process_all_housekeeping(data: bytes) ‑> Union[List[List[~T]], Failure] +def process_all_housekeeping(response: bytes) ‑> Union[List[List[~T]], Failure]

    Process the response from the TCS EGSE remote command get_TM_All.

    Args

    -
    data : str
    -
    a string containing the response from the TCS EGSE to the get_TM_All command.
    +
    response : bytes
    +
    contains the response from the TCS EGSE to the get_TM_All command.

    Returns

    A nested list where the inner lists contain the name, timestamp and value of a housekeeping @@ -1107,12 +1112,12 @@

    Returns

    Expand source code -
    def process_all_housekeeping(data: bytes) -> Union[List[List], Failure]:
    +
    def process_all_housekeeping(response: bytes) -> Union[List[List], Failure]:
         """
         Process the response from the TCS EGSE remote command `get_TM_All`.
     
         Args:
    -        data (str): a string containing the response from the TCS EGSE to the get_TM_All command.
    +        response (bytes): contains the response from the TCS EGSE to the get_TM_All command.
     
         Returns:
             A nested list where the inner lists contain the name, timestamp and value of a housekeeping
    @@ -1122,7 +1127,7 @@ 

    Returns

    communication error occurred. """ - data = data.decode().split('\x03') + data = response.decode().split('\x03') if "not_allowed_remote_mode_not_active" in data: msg = "Remote Control not active, no housekeeping values received." @@ -1157,14 +1162,14 @@

    Returns

    -def process_configuration(tcs_conf: bytes) ‑> Union[Dict[~KT, ~VT], Failure] +def process_configuration(response: bytes) ‑> Union[Dict[~KT, ~VT], Failure]

    Process the response to the get_configuration command. Processing in this context means to disentangle the returned string and split it up in parameter name, value pairs.

    Args

    -
    tcs_conf : str
    +
    response : str
    a string containing the response from the TCS EGSE

    Returns

    @@ -1173,13 +1178,13 @@

    Returns

    Expand source code -
    def process_configuration(tcs_conf: bytes) -> Union[Dict, Failure]:
    +
    def process_configuration(response: bytes) -> Union[Dict, Failure]:
         """
         Process the response to the get_configuration command. Processing in this context means to
         disentangle the returned string and split it up in parameter name, value pairs.
     
         Args:
    -        tcs_conf (str): a string containing the response from the TCS EGSE
    +        response (str): a string containing the response from the TCS EGSE
     
         Returns:
             A dictionary with the parameters names and their current values.
    @@ -1188,7 +1193,7 @@ 

    Returns

    # The rest of this function parse and organise the configuration parameters # in a list of lists with [[name, value], [...]] - tcs_conf = tcs_conf.decode().split('\x03') + tcs_conf = response.decode().split('\x03') # Remove empty items from the list. The list ends always with an empty item because # the response ends with the '\x03' EOT character. @@ -1311,7 +1316,7 @@

    Returns

    -def process_get_housekeeping_value(data: bytes) ‑> Union[HousekeepingValueFailure] +def process_get_housekeeping_value(response: bytes) ‑> Union[HousekeepingValueFailure]
    @@ -1319,15 +1324,15 @@

    Returns

    Expand source code -
    def process_get_housekeeping_value(data: bytes) -> Union[HousekeepingValue, Failure]:
    +
    def process_get_housekeeping_value(response: bytes) -> Union[HousekeepingValue, Failure]:
     
    -    if b"remote_mode_not_active" in data:
    +    if b"remote_mode_not_active" in response:
             logger.warning("Requesting housekeeping value invalid, not in remote control mode.")
             return Failure("Requesting housekeeping value invalid, not in remote control mode.")
    -    if b"invalid_tm_item_id" in data:
    +    if b"invalid_tm_item_id" in response:
             logger.warning("Invalid housekeeping parameter name.")
             return Failure("Invalid housekeeping parameter name.")
    -    data = data.decode().split('\x03')
    +    data = response.decode().split('\x03')
         data = data[0].split('\t')
         return HousekeepingValue(*data)
    @@ -2457,7 +2462,7 @@

    Inherited members

    class TCSParameterNaming -(origin: str) +(origin: str, setup: Setup)

    Defines the mapping between TCS EGSE device names and the CGSE names.

    @@ -2467,12 +2472,13 @@

    Inherited members

    class TCSParameterNaming:
         """Defines the mapping between TCS EGSE device names and the CGSE names."""
    -    def __init__(self, origin: str):
    +    def __init__(self, origin: str, setup: Setup):
    +
             # The hk_names_mapping is a dictionary that maps the original device telemetry parameter
             # names to the correct device names as defined in the CGSE. The keys in the mapping are
             # the original device name, the values are the CGSE corrected names.
     
    -        self.hk_names_mapping = read_conversion_dict(storage_mnemonic=origin, use_site=False)
    +        self.hk_names_mapping = read_conversion_dict(storage_mnemonic=origin, use_site=False, setup=setup)
     
         def get_all_cgse_names(self):
             """Returns the correct CGSE housekeeping parameter names."""
    @@ -2685,7 +2691,7 @@ 

    Inherited members

    class TCSTelemetry -(command_queue: >, response_queue: >) +(command_queue: >, response_queue: >)

    Process objects represent activity that is run in a separate process

    @@ -2706,6 +2712,7 @@

    Inherited members

    self._killer = None self._metrics: TCSTelemetryMetrics = None self._hk_names: TCSParameterNaming = None + self.setup = load_setup() def run(self): @@ -2718,15 +2725,16 @@

    Inherited members

    self._killer = SignalCatcher() + # Device naming is different from CGSE naming, the conversion is handled in the # TCSParameterNaming class. - self._hk_names = TCSParameterNaming(origin=ORIGIN) + self._hk_names = TCSParameterNaming(origin=ORIGIN, setup=self.setup) # Metrics must be defined only when the sub-process already exists, otherwise a TypeError # will be raised, indicating that a thread lock cannot be pickled - self._metrics = TCSTelemetryMetrics() + self._metrics = TCSTelemetryMetrics(self.setup) # These are only 61 of the telemetry parameters of the TCS EGSE. It is not possible to know # all the parameters beforehand since they are only published when changed. So, eventually, @@ -2912,15 +2920,16 @@

    Returns

    self._killer = SignalCatcher() + # Device naming is different from CGSE naming, the conversion is handled in the # TCSParameterNaming class. - self._hk_names = TCSParameterNaming(origin=ORIGIN) + self._hk_names = TCSParameterNaming(origin=ORIGIN, setup=self.setup) # Metrics must be defined only when the sub-process already exists, otherwise a TypeError # will be raised, indicating that a thread lock cannot be pickled - self._metrics = TCSTelemetryMetrics() + self._metrics = TCSTelemetryMetrics(self.setup) # These are only 61 of the telemetry parameters of the TCS EGSE. It is not possible to know # all the parameters beforehand since they are only published when changed. So, eventually, @@ -2977,6 +2986,7 @@

    Returns

    class TCSTelemetryMetrics +(setup: Setup)

    Defines the metrics for the TCS EGSE that are maintained by the TCSTelemetry class.

    @@ -2986,9 +2996,10 @@

    Returns

    class TCSTelemetryMetrics:
         """Defines the metrics for the TCS EGSE that are maintained by the TCSTelemetry class."""
    -    def __init__(self):
    -        self.hk_metrics = define_metrics(origin=ORIGIN, dashboard="TCS_TRP_MON")
    -        self.hk_metrics_status = define_metrics(origin=ORIGIN, dashboard="TCS_STATUS_MON")
    +    def __init__(self, setup: Setup):
    +
    +        self.hk_metrics = define_metrics(origin=ORIGIN, dashboard="TCS_TRP_MON", setup=setup)
    +        self.hk_metrics_status = define_metrics(origin=ORIGIN, dashboard="TCS_STATUS_MON", setup=setup)
     
         def update_metrics(self, name: str, value: str):
             """Update the TCS metric parameter with the given value."""
    diff --git a/docs/api/egse/tcs/tcs_cs.html b/docs/api/egse/tcs/tcs_cs.html
    index a131326..007f589 100644
    --- a/docs/api/egse/tcs/tcs_cs.html
    +++ b/docs/api/egse/tcs/tcs_cs.html
    @@ -472,10 +472,15 @@ 

    Inherited members

    • ControlServer:
    • diff --git a/docs/api/egse/tcs/tcs_protocol.html b/docs/api/egse/tcs/tcs_protocol.html index 93033a0..9cdfffc 100644 --- a/docs/api/egse/tcs/tcs_protocol.html +++ b/docs/api/egse/tcs/tcs_protocol.html @@ -39,6 +39,7 @@

      Module egse.tcs.tcs_protocol

      from egse.metrics import define_metrics from egse.protocol import DynamicCommandProtocol from egse.settings import Settings +from egse.setup import load_setup, Setup from egse.system import format_datetime from egse.tcs.tcs import TCSController from egse.tcs.tcs import TCSParameterNaming @@ -59,8 +60,8 @@

      Module egse.tcs.tcs_protocol

      class TCSMetrics: """Defines the metrics for the TCS EGSE that are maintained by the TCSProtocol class.""" - def __init__(self, origin: str, dashboard: str): - self.metrics = define_metrics(origin=origin, dashboard=dashboard) + def __init__(self, origin: str, dashboard: str, setup: Setup): + self.metrics = define_metrics(origin=origin, dashboard=dashboard, setup=setup) def __contains__(self, item): return self.metrics.__contains__(item) @@ -78,15 +79,17 @@

      Module egse.tcs.tcs_protocol

      def __init__(self, control_server: ControlServer): super().__init__(control_server) + setup = load_setup() + # Read the configuration metrics from the tm-dictionary self._metrics = TCSMetrics(origin=control_server.get_storage_mnemonic(), - dashboard="TCS_CONFIGURATION_MON") + dashboard="TCS_CONFIGURATION_MON", setup=setup) # Device naming is different from CGSE naming, the conversion is handled in the # TCSParameterNaming class. - self._hk_names = TCSParameterNaming(origin=control_server.get_storage_mnemonic()) + self._hk_names = TCSParameterNaming(origin=control_server.get_storage_mnemonic(), setup=setup) # Set up two queue's to communicate with the TCS Telemetry Process. # The command queue is joinable because the Controller needs to wait for a response in @@ -268,7 +271,7 @@

      Inherited members

    class TCSMetrics -(origin: str, dashboard: str) +(origin: str, dashboard: str, setup: Setup)

    Defines the metrics for the TCS EGSE that are maintained by the TCSProtocol class.

    @@ -278,8 +281,8 @@

    Inherited members

    class TCSMetrics:
         """Defines the metrics for the TCS EGSE that are maintained by the TCSProtocol class."""
    -    def __init__(self, origin: str, dashboard: str):
    -        self.metrics = define_metrics(origin=origin, dashboard=dashboard)
    +    def __init__(self, origin: str, dashboard: str, setup: Setup):
    +        self.metrics = define_metrics(origin=origin, dashboard=dashboard, setup=setup)
     
         def __contains__(self, item):
             return self.metrics.__contains__(item)
    @@ -340,15 +343,17 @@ 

    Methods

    def __init__(self, control_server: ControlServer): super().__init__(control_server) + setup = load_setup() + # Read the configuration metrics from the tm-dictionary self._metrics = TCSMetrics(origin=control_server.get_storage_mnemonic(), - dashboard="TCS_CONFIGURATION_MON") + dashboard="TCS_CONFIGURATION_MON", setup=setup) # Device naming is different from CGSE naming, the conversion is handled in the # TCSParameterNaming class. - self._hk_names = TCSParameterNaming(origin=control_server.get_storage_mnemonic()) + self._hk_names = TCSParameterNaming(origin=control_server.get_storage_mnemonic(), setup=setup) # Set up two queue's to communicate with the TCS Telemetry Process. # The command queue is joinable because the Controller needs to wait for a response in diff --git a/docs/api/egse/tcs/tcs_ui.html b/docs/api/egse/tcs/tcs_ui.html index 0f2815a..b2e7eff 100644 --- a/docs/api/egse/tcs/tcs_ui.html +++ b/docs/api/egse/tcs/tcs_ui.html @@ -1167,7 +1167,7 @@

    Methods

    class TCSUIView
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code diff --git a/docs/api/egse/tempcontrol/agilent/agilent34970_cs.html b/docs/api/egse/tempcontrol/agilent/agilent34970_cs.html index 72c473a..a443907 100644 --- a/docs/api/egse/tempcontrol/agilent/agilent34970_cs.html +++ b/docs/api/egse/tempcontrol/agilent/agilent34970_cs.html @@ -27,21 +27,19 @@

    Module egse.tempcontrol.agilent.agilent34970_csExpand source code

    #!/usr/bin/env python3
    -import argparse
     import logging
    -import sys
    -import click
    +import multiprocessing
     
    +import click
    +import sys
     import zmq
     from prometheus_client import start_http_server
     
    -import multiprocessing
     multiprocessing.current_process().name = "agilent34970_cs"
     
     from egse.control import ControlServer
     from egse.settings import Settings
    -from egse.zmq_ser import connect_address
    -from egse.tempcontrol.agilent.agilent34970 import Agilent34970Interface, Agilent34970Proxy
    +from egse.tempcontrol.agilent.agilent34970 import Agilent34970Proxy
     from egse.tempcontrol.agilent.agilent34970_protocol import Agilent34970Protocol
     
     logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL)
    @@ -301,10 +299,15 @@ 

    Inherited members

    • ControlServer:
    • diff --git a/docs/api/egse/tempcontrol/agilent/agilent34970_devif.html b/docs/api/egse/tempcontrol/agilent/agilent34970_devif.html index 8c45610..2438599 100644 --- a/docs/api/egse/tempcontrol/agilent/agilent34970_devif.html +++ b/docs/api/egse/tempcontrol/agilent/agilent34970_devif.html @@ -29,13 +29,13 @@

      Module egse.tempcontrol.agilent.agilent34970_devif#!/usr/bin/env python3 import logging + import time from egse.command import ClientServerCommand -from egse.exceptions import DeviceNotFoundError from egse.serialdevice import SerialDevice from egse.settings import Settings -from egse.setup import get_setup +from egse.setup import load_setup logger = logging.getLogger(__name__) @@ -56,7 +56,7 @@

      Module egse.tempcontrol.agilent.agilent34970_devifInherited members

      def __init__(self, device_index): self._ctrl_settings = Settings.load(f'Agilent 34970 Controller') - self._setup = get_setup() + self._setup = load_setup() self.device_index = device_index diff --git a/docs/api/egse/tempcontrol/agilent/agilent34970_protocol.html b/docs/api/egse/tempcontrol/agilent/agilent34970_protocol.html index 2061fd9..ef49d92 100644 --- a/docs/api/egse/tempcontrol/agilent/agilent34970_protocol.html +++ b/docs/api/egse/tempcontrol/agilent/agilent34970_protocol.html @@ -32,16 +32,13 @@

      Module egse.tempcontrol.agilent.agilent34970_protocolModule egse.tempcontrol.agilent.agilent34970_protocolClasses

      self.build_device_method_lookup_table(self.agilent) # Get scan list from setup - setup = GlobalState.setup + setup = load_setup() self._setup = setup['gse'][f'agilent34970_{device_index}'] self._conversions = self._setup['conversion'] diff --git a/docs/api/egse/tempcontrol/agilent/agilent34972_cs.html b/docs/api/egse/tempcontrol/agilent/agilent34972_cs.html index 593950d..87f55d3 100644 --- a/docs/api/egse/tempcontrol/agilent/agilent34972_cs.html +++ b/docs/api/egse/tempcontrol/agilent/agilent34972_cs.html @@ -27,21 +27,19 @@

      Module egse.tempcontrol.agilent.agilent34972_csExpand source code
      #!/usr/bin/env python3
      -import argparse
       import logging
      -import sys
      -import click
      +import multiprocessing
       
      +import click
      +import sys
       import zmq
       from prometheus_client import start_http_server
       
      -import multiprocessing
       multiprocessing.current_process().name = "agilent34972_cs"
       
       from egse.control import ControlServer
       from egse.settings import Settings
      -from egse.zmq_ser import connect_address
      -from egse.tempcontrol.agilent.agilent34972 import Agilent34972Interface, Agilent34972Proxy
      +from egse.tempcontrol.agilent.agilent34972 import Agilent34972Proxy
       from egse.tempcontrol.agilent.agilent34972_protocol import Agilent34972Protocol
       
       
      @@ -302,10 +300,15 @@ 

      Inherited members

      • ControlServer:
      • diff --git a/docs/api/egse/tempcontrol/agilent/agilent34972_devif.html b/docs/api/egse/tempcontrol/agilent/agilent34972_devif.html index 62c814f..7a68b28 100644 --- a/docs/api/egse/tempcontrol/agilent/agilent34972_devif.html +++ b/docs/api/egse/tempcontrol/agilent/agilent34972_devif.html @@ -29,12 +29,12 @@

        Module egse.tempcontrol.agilent.agilent34972_devif#!/usr/bin/env python3 import logging + import time from egse.command import ClientServerCommand -from egse.exceptions import DeviceNotFoundError from egse.settings import Settings -from egse.setup import get_setup +from egse.setup import load_setup from egse.socketdevice import SocketDevice logger = logging.getLogger(__name__) @@ -54,7 +54,7 @@

        Module egse.tempcontrol.agilent.agilent34972_devifInherited members

        def __init__(self, device_index): self._ctrl_settings = Settings.load(f'Agilent 34972 Controller') - self._setup = get_setup() + self._setup = load_setup() self.device_index = device_index diff --git a/docs/api/egse/tempcontrol/agilent/agilent34972_protocol.html b/docs/api/egse/tempcontrol/agilent/agilent34972_protocol.html index 67df7d0..8f82581 100644 --- a/docs/api/egse/tempcontrol/agilent/agilent34972_protocol.html +++ b/docs/api/egse/tempcontrol/agilent/agilent34972_protocol.html @@ -30,11 +30,9 @@

        Module egse.tempcontrol.agilent.agilent34972_protocolModule egse.tempcontrol.agilent.agilent34972_protocolModule egse.tempcontrol.agilent.agilent34972_protocolClasses

        self.build_device_method_lookup_table(self.agilent) # Get scan list from setup - setup = get_setup() + setup = load_setup() self._setup = setup['gse'][f'agilent34972_{device_index}'] self._conversions = self._setup['conversion'] diff --git a/docs/api/egse/tempcontrol/beaglebone/beaglebone.html b/docs/api/egse/tempcontrol/beaglebone/beaglebone.html deleted file mode 100644 index 1755150..0000000 --- a/docs/api/egse/tempcontrol/beaglebone/beaglebone.html +++ /dev/null @@ -1,830 +0,0 @@ - - - - - - -egse.tempcontrol.beaglebone.beaglebone API documentation - - - - - - - - - - - -
        -
        -
        -

        Module egse.tempcontrol.beaglebone.beaglebone

        -
        -
        -
        - -Expand source code - -
        import logging
        -
        -from egse.decorators import dynamic_interface
        -from egse.device import DeviceInterface
        -from egse.proxy import Proxy
        -from egse.settings import Settings
        -from egse.tempcontrol.beaglebone.beaglebone_devif import BeagleboneDeviceInterface
        -from egse.zmq_ser import connect_address
        -
        -logger = logging.getLogger(__name__)
        -
        -CTRL_SETTINGS = Settings.load("BeagleBone Heater Control Server")
        -DEVICE_SETTINGS = Settings.load(filename='beaglebone.yaml')
        -
        -
        -class BeagleboneError(Exception):
        -    pass
        -
        -
        -class BeagleboneInterface(DeviceInterface):
        -    """ BeagleBone base class."""
        -
        -    @dynamic_interface
        -    def set_enable(self, dev_idx, chnl_idx, enable):
        -        """ Enable PWM pin. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def set_duty_cycle(self, dev_idx, chnl_idx, duty_cycle):
        -        """ Duty cycle duration in ns. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def set_period(self, dev_idx, chnl_idx, period):
        -        """ Period duration in ns. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def get_temperature(self, dev_idx):
        -        """ Get board temperature. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def get_voltage(self, dev_idx, chnl_idx):
        -        """ Get channel voltage. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def get_current(self, dev_idx, chnl_idx):
        -        """ Get channel current. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def get_power(self, dev_idx, chnl_idx):
        -        """ Get channel power. """
        -        return NotImplemented
        -    
        -    @dynamic_interface
        -    def get_enable(self, dev_idx, chnl_idx):
        -        """ Get enabled state. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def get_duty_cycle(self, dev_idx, chnl_idx):
        -        """ Get duty cycle. """
        -        return NotImplemented
        -
        -
        -class BeagleboneSimulator(BeagleboneInterface):
        -   """ BeagleBone simulator class. """
        -
        -   def __init__(self):
        -        self._is_connected = True
        -        self.duty_cycle = [[[] for _ in range(4)] for _ in range(6)]
        -        self.enabled = [[[] for _ in range(4)] for _ in range(6)]
        -
        -        for htr_idx in range(0, 6):
        -            for ch_idx in range(0, 4):
        -                self.duty_cycle[htr_idx][ch_idx] = 2 * ch_idx + htr_idx
        -                self.enabled[htr_idx][ch_idx] = False
        -
        -
        -   def is_connected(self):
        -        return self._is_connected
        -
        -   def is_simulator(self):
        -        return True
        -
        -   def connect(self):
        -        self._is_connected = True
        -
        -   def disconnect(self):
        -        self._is_connected = False
        -
        -   def reconnect(self):
        -       if self.is_connected():
        -           self.disconnect()
        -       self.connect()
        -
        -   def set_enable(self, dev_idx, chnl_idx, enable):
        -       logger.info(f"Heater {dev_idx} Channel {chnl_idx} state has been set to {enable}")
        -       self.enabled[dev_idx][chnl_idx] = enable
        -
        -   def set_duty_cycle(self, dev_idx, chnl_idx, duty_cycle):
        -       logger.info(f"Duty cycle for Heater {dev_idx} Channel {chnl_idx} set to {duty_cycle}ns")
        -       self.duty_cycle[dev_idx][chnl_idx] = duty_cycle
        -
        -   def set_period(self, dev_idx, chnl_idx, period):
        -       logger.info(f"Period for Heater {dev_idx} Channel {chnl_idx} set to {period} ns")
        -
        -
        -   def get_temperature(self, dev_idx):
        -       return 20.0
        -
        -   def get_voltage(self, dev_idx, chnl_idx):
        -       return 1.0
        -
        -   def get_current(self, dev_idx, chnl_idx):
        -       return 0.1
        -
        -   def get_power(self, dev_idx, chnl_idx):
        -       return 1.0
        -   
        -   def get_enable(self, dev_idx, chnl_idx):
        -       return self.enabled[dev_idx][chnl_idx]
        -   
        -   def get_duty_cycle(self, dev_idx, chnl_idx):
        -       return self.duty_cycle[dev_idx][chnl_idx]
        -   
        -   def get_availability(self):
        -       return [True] * 6
        -
        -
        -class BeagleboneController(BeagleboneInterface):
        -
        -    def __init__(self):
        -        super().__init__()
        -
        -        logger.debug('Initalizing BeagleBone Controller')
        -
        -        try:
        -            self.beaglebone = BeagleboneDeviceInterface()
        -            self.beaglebone.connect()
        -        except BeagleboneError as exc:
        -            logger.warning(f"BeagleboneError caught: Couldn't establish connection ({exc})")
        -            raise BeagleboneError(
        -                "Couldn't establish a connection with BeagleBone controller."
        -            ) from exc
        -
        -    def is_simulator(self):
        -        return False
        -
        -    def is_connected(self):
        -        return self.beaglebone.is_connected()
        -
        -    def connect(self):
        -        if not self.beaglebone.is_connected():
        -            self.beaglebone.connect()
        -
        -    def disconnect(self):
        -        self.beaglebone.disconnect()
        -
        -    def reconnect(self):
        -        self.beaglebone.reconnect()
        -
        -    def set_enable(self, dev_idx, chnl_idx, enable):
        -        return self.beaglebone.set_enable(dev_idx, chnl_idx, enable)
        -
        -    def set_duty_cycle(self, dev_idx, chnl_idx, duty_cycle):
        -        return self.beaglebone.set_duty_cycle(dev_idx, chnl_idx, duty_cycle)
        -
        -    def set_period(self, dev_idx, chnl_idx, period):
        -        return self.beaglebone.set_period(dev_idx, chnl_idx, period)
        -
        -    def get_temperature(self, dev_idx):
        -        return self.beaglebone.get_temperature(dev_idx)
        -
        -    def get_voltage(self, dev_idx, chnl_idx):
        -        return self.beaglebone.get_voltage(dev_idx, chnl_idx)
        -
        -    def get_current(self, dev_idx, chnl_idx):
        -        return self.beaglebone.get_current(dev_idx, chnl_idx)
        -
        -    def get_power(self, dev_idx, chnl_idx):
        -        return self.beaglebone.get_power(dev_idx, chnl_idx)
        -    
        -    def get_enable(self, dev_idx, chnl_idx):
        -        return self.beaglebone.get_enable(dev_idx, chnl_idx)
        -    
        -    def get_duty_cycle(self, dev_idx, chnl_idx):
        -        return self.beaglebone.get_duty_cycle(dev_idx, chnl_idx)
        -
        -
        -class BeagleboneProxy(Proxy, BeagleboneInterface):
        -    def __init__(self, protocol=CTRL_SETTINGS.PROTOCOL,
        -                 hostname=CTRL_SETTINGS.HOSTNAME,
        -                 port=CTRL_SETTINGS.COMMANDING_PORT):
        -        super().__init__(connect_address(protocol, hostname, port))
        -
        -
        -
        -
        -
        -
        -
        -
        -
        -

        Classes

        -
        -
        -class BeagleboneController -
        -
        -

        BeagleBone base class.

        -
        - -Expand source code - -
        class BeagleboneController(BeagleboneInterface):
        -
        -    def __init__(self):
        -        super().__init__()
        -
        -        logger.debug('Initalizing BeagleBone Controller')
        -
        -        try:
        -            self.beaglebone = BeagleboneDeviceInterface()
        -            self.beaglebone.connect()
        -        except BeagleboneError as exc:
        -            logger.warning(f"BeagleboneError caught: Couldn't establish connection ({exc})")
        -            raise BeagleboneError(
        -                "Couldn't establish a connection with BeagleBone controller."
        -            ) from exc
        -
        -    def is_simulator(self):
        -        return False
        -
        -    def is_connected(self):
        -        return self.beaglebone.is_connected()
        -
        -    def connect(self):
        -        if not self.beaglebone.is_connected():
        -            self.beaglebone.connect()
        -
        -    def disconnect(self):
        -        self.beaglebone.disconnect()
        -
        -    def reconnect(self):
        -        self.beaglebone.reconnect()
        -
        -    def set_enable(self, dev_idx, chnl_idx, enable):
        -        return self.beaglebone.set_enable(dev_idx, chnl_idx, enable)
        -
        -    def set_duty_cycle(self, dev_idx, chnl_idx, duty_cycle):
        -        return self.beaglebone.set_duty_cycle(dev_idx, chnl_idx, duty_cycle)
        -
        -    def set_period(self, dev_idx, chnl_idx, period):
        -        return self.beaglebone.set_period(dev_idx, chnl_idx, period)
        -
        -    def get_temperature(self, dev_idx):
        -        return self.beaglebone.get_temperature(dev_idx)
        -
        -    def get_voltage(self, dev_idx, chnl_idx):
        -        return self.beaglebone.get_voltage(dev_idx, chnl_idx)
        -
        -    def get_current(self, dev_idx, chnl_idx):
        -        return self.beaglebone.get_current(dev_idx, chnl_idx)
        -
        -    def get_power(self, dev_idx, chnl_idx):
        -        return self.beaglebone.get_power(dev_idx, chnl_idx)
        -    
        -    def get_enable(self, dev_idx, chnl_idx):
        -        return self.beaglebone.get_enable(dev_idx, chnl_idx)
        -    
        -    def get_duty_cycle(self, dev_idx, chnl_idx):
        -        return self.beaglebone.get_duty_cycle(dev_idx, chnl_idx)
        -
        -

        Ancestors

        - -

        Inherited members

        - -
        -
        -class BeagleboneError -(*args, **kwargs) -
        -
        -

        Common base class for all non-exit exceptions.

        -
        - -Expand source code - -
        class BeagleboneError(Exception):
        -    pass
        -
        -

        Ancestors

        -
          -
        • builtins.Exception
        • -
        • builtins.BaseException
        • -
        -
        -
        -class BeagleboneInterface -
        -
        -

        BeagleBone base class.

        -
        - -Expand source code - -
        class BeagleboneInterface(DeviceInterface):
        -    """ BeagleBone base class."""
        -
        -    @dynamic_interface
        -    def set_enable(self, dev_idx, chnl_idx, enable):
        -        """ Enable PWM pin. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def set_duty_cycle(self, dev_idx, chnl_idx, duty_cycle):
        -        """ Duty cycle duration in ns. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def set_period(self, dev_idx, chnl_idx, period):
        -        """ Period duration in ns. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def get_temperature(self, dev_idx):
        -        """ Get board temperature. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def get_voltage(self, dev_idx, chnl_idx):
        -        """ Get channel voltage. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def get_current(self, dev_idx, chnl_idx):
        -        """ Get channel current. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def get_power(self, dev_idx, chnl_idx):
        -        """ Get channel power. """
        -        return NotImplemented
        -    
        -    @dynamic_interface
        -    def get_enable(self, dev_idx, chnl_idx):
        -        """ Get enabled state. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def get_duty_cycle(self, dev_idx, chnl_idx):
        -        """ Get duty cycle. """
        -        return NotImplemented
        -
        -

        Ancestors

        - -

        Subclasses

        - -

        Methods

        -
        -
        -def get_current(self, dev_idx, chnl_idx) -
        -
        -

        Get channel current.

        -
        - -Expand source code - -
        @dynamic_interface
        -def get_current(self, dev_idx, chnl_idx):
        -    """ Get channel current. """
        -    return NotImplemented
        -
        -
        -
        -def get_duty_cycle(self, dev_idx, chnl_idx) -
        -
        -

        Get duty cycle.

        -
        - -Expand source code - -
        @dynamic_interface
        -def get_duty_cycle(self, dev_idx, chnl_idx):
        -    """ Get duty cycle. """
        -    return NotImplemented
        -
        -
        -
        -def get_enable(self, dev_idx, chnl_idx) -
        -
        -

        Get enabled state.

        -
        - -Expand source code - -
        @dynamic_interface
        -def get_enable(self, dev_idx, chnl_idx):
        -    """ Get enabled state. """
        -    return NotImplemented
        -
        -
        -
        -def get_power(self, dev_idx, chnl_idx) -
        -
        -

        Get channel power.

        -
        - -Expand source code - -
        @dynamic_interface
        -def get_power(self, dev_idx, chnl_idx):
        -    """ Get channel power. """
        -    return NotImplemented
        -
        -
        -
        -def get_temperature(self, dev_idx) -
        -
        -

        Get board temperature.

        -
        - -Expand source code - -
        @dynamic_interface
        -def get_temperature(self, dev_idx):
        -    """ Get board temperature. """
        -    return NotImplemented
        -
        -
        -
        -def get_voltage(self, dev_idx, chnl_idx) -
        -
        -

        Get channel voltage.

        -
        - -Expand source code - -
        @dynamic_interface
        -def get_voltage(self, dev_idx, chnl_idx):
        -    """ Get channel voltage. """
        -    return NotImplemented
        -
        -
        -
        -def set_duty_cycle(self, dev_idx, chnl_idx, duty_cycle) -
        -
        -

        Duty cycle duration in ns.

        -
        - -Expand source code - -
        @dynamic_interface
        -def set_duty_cycle(self, dev_idx, chnl_idx, duty_cycle):
        -    """ Duty cycle duration in ns. """
        -    return NotImplemented
        -
        -
        -
        -def set_enable(self, dev_idx, chnl_idx, enable) -
        -
        -

        Enable PWM pin.

        -
        - -Expand source code - -
        @dynamic_interface
        -def set_enable(self, dev_idx, chnl_idx, enable):
        -    """ Enable PWM pin. """
        -    return NotImplemented
        -
        -
        -
        -def set_period(self, dev_idx, chnl_idx, period) -
        -
        -

        Period duration in ns.

        -
        - -Expand source code - -
        @dynamic_interface
        -def set_period(self, dev_idx, chnl_idx, period):
        -    """ Period duration in ns. """
        -    return NotImplemented
        -
        -
        -
        -

        Inherited members

        - -
        -
        -class BeagleboneProxy -(protocol='tcp', hostname='localhost', port=6930) -
        -
        -

        A Proxy object will forward CommandExecutions to the connected control server -and wait for a response. When the Proxy can not connect to its control server -during initialization, a ConnectionError will be raised.

        -

        During initialization, the Proxy will connect to the control server and send a -handshaking Ping command. When that succeeds the Proxy will request and load the -available commands from the control server. When the connection with the control server -fails, no commands are loaded and the Proxy is left in a 'disconnected' state. The caller -can fix the problem with the control server and call connect_cs(), followed by a call to -load_commands().

        -

        The timeout argument specifies the number of milliseconds

        -
        - -Expand source code - -
        class BeagleboneProxy(Proxy, BeagleboneInterface):
        -    def __init__(self, protocol=CTRL_SETTINGS.PROTOCOL,
        -                 hostname=CTRL_SETTINGS.HOSTNAME,
        -                 port=CTRL_SETTINGS.COMMANDING_PORT):
        -        super().__init__(connect_address(protocol, hostname, port))
        -
        -

        Ancestors

        - -

        Inherited members

        - -
        -
        -class BeagleboneSimulator -
        -
        -

        BeagleBone simulator class.

        -
        - -Expand source code - -
        class BeagleboneSimulator(BeagleboneInterface):
        -   """ BeagleBone simulator class. """
        -
        -   def __init__(self):
        -        self._is_connected = True
        -        self.duty_cycle = [[[] for _ in range(4)] for _ in range(6)]
        -        self.enabled = [[[] for _ in range(4)] for _ in range(6)]
        -
        -        for htr_idx in range(0, 6):
        -            for ch_idx in range(0, 4):
        -                self.duty_cycle[htr_idx][ch_idx] = 2 * ch_idx + htr_idx
        -                self.enabled[htr_idx][ch_idx] = False
        -
        -
        -   def is_connected(self):
        -        return self._is_connected
        -
        -   def is_simulator(self):
        -        return True
        -
        -   def connect(self):
        -        self._is_connected = True
        -
        -   def disconnect(self):
        -        self._is_connected = False
        -
        -   def reconnect(self):
        -       if self.is_connected():
        -           self.disconnect()
        -       self.connect()
        -
        -   def set_enable(self, dev_idx, chnl_idx, enable):
        -       logger.info(f"Heater {dev_idx} Channel {chnl_idx} state has been set to {enable}")
        -       self.enabled[dev_idx][chnl_idx] = enable
        -
        -   def set_duty_cycle(self, dev_idx, chnl_idx, duty_cycle):
        -       logger.info(f"Duty cycle for Heater {dev_idx} Channel {chnl_idx} set to {duty_cycle}ns")
        -       self.duty_cycle[dev_idx][chnl_idx] = duty_cycle
        -
        -   def set_period(self, dev_idx, chnl_idx, period):
        -       logger.info(f"Period for Heater {dev_idx} Channel {chnl_idx} set to {period} ns")
        -
        -
        -   def get_temperature(self, dev_idx):
        -       return 20.0
        -
        -   def get_voltage(self, dev_idx, chnl_idx):
        -       return 1.0
        -
        -   def get_current(self, dev_idx, chnl_idx):
        -       return 0.1
        -
        -   def get_power(self, dev_idx, chnl_idx):
        -       return 1.0
        -   
        -   def get_enable(self, dev_idx, chnl_idx):
        -       return self.enabled[dev_idx][chnl_idx]
        -   
        -   def get_duty_cycle(self, dev_idx, chnl_idx):
        -       return self.duty_cycle[dev_idx][chnl_idx]
        -   
        -   def get_availability(self):
        -       return [True] * 6
        -
        -

        Ancestors

        - -

        Methods

        -
        -
        -def get_availability(self) -
        -
        -
        -
        - -Expand source code - -
        def get_availability(self):
        -    return [True] * 6
        -
        -
        -
        -

        Inherited members

        - -
        -
        -
        -
        - -
        - - - \ No newline at end of file diff --git a/docs/api/egse/tempcontrol/beaglebone/beaglebone_devif.html b/docs/api/egse/tempcontrol/beaglebone/beaglebone_devif.html deleted file mode 100644 index 750e22c..0000000 --- a/docs/api/egse/tempcontrol/beaglebone/beaglebone_devif.html +++ /dev/null @@ -1,647 +0,0 @@ - - - - - - -egse.tempcontrol.beaglebone.beaglebone_devif API documentation - - - - - - - - - - - -
        -
        -
        -

        Module egse.tempcontrol.beaglebone.beaglebone_devif

        -
        -
        -
        - -Expand source code - -
        import logging
        -import struct
        -import time
        -import socket
        -import errno
        -import os
        -
        -from egse.command import ClientServerCommand
        -from egse.confman import ConfigurationManagerProxy
        -from egse.exceptions import DeviceNotFoundError
        -from egse.settings import Settings
        -from egse.confman import ConfigurationManagerProxy
        -
        -
        -logger = logging.getLogger(__name__)
        -ctrl_settings = Settings.load('BeagleBone Heater Control Server')
        -
        -LED_PINS = [69, 68, 66, 67]
        -PWM_PINS = [4, 5, 7, 8]
        -
        -class BeagleboneCommand(ClientServerCommand):
        -    def get_cmd_string(self, *args, **kwargs):
        -        out = super().get_cmd_string(*args, **kwargs)
        -        return out + '\n'
        -
        -
        -class BeagleboneDeviceInterface:
        -    def __init__(self):
        -
        -        # Perform SRON gssw imports here to avoid issues at other THs
        -        from gssw.config.configclient import ConfigClient
        -        from gssw.common import addLoggingLevel
        -        from gssw.hal.hal import Hal
        -        from gssw.hal.halclient import HalClient
        -        from gssw.lib.device import Device
        -
        -        self._is_connected = False
        -
        -        # InitGssw does something to zmq that breaks cgse logging.
        -        # So instead I copied the essentials here.
        -        configClient = ConfigClient(os.getenv('GSSW_CONFIGURATION_FILE'))
        -        config = configClient.config
        -        addLoggingLevel('data', 15)
        -
        -        with ConfigurationManagerProxy() as cm:
        -            setup = cm.get_setup()
        -
        -        self._availability = setup.gse.beaglebone_heater.availability
        -
        -        hal = Hal()
        -        hal_client = HalClient(hal, config)
        -        hal_client.requestHal()
        -
        -        self.dev_pwm_names = [f'pwm_{dev_idx+1}' for dev_idx, available in enumerate(self._availability) if available]
        -        self.dev_i2c_names = [f'i2c_{dev_idx+1}' for dev_idx, available in enumerate(self._availability)  if available]
        -        self.dev_gpio_names = [f'gpio_{dev_idx+1}' for dev_idx, available in enumerate(self._availability) if available]
        -
        -        self._dev_pwm = [None, None, None, None, None, None]
        -        for dev_idx, dev_name in enumerate(self.dev_pwm_names):
        -            self._dev_pwm[dev_idx] = Device(dev_name, config, hal)
        -
        -        self._dev_i2c = [None, None, None, None, None, None]
        -        for dev_idx, dev_name in enumerate(self.dev_i2c_names):
        -            self._dev_i2c[dev_idx] = Device(dev_name, config, hal)
        -
        -        self._dev_gpio = [None, None, None, None, None, None]
        -        for dev_idx, dev_name in enumerate(self.dev_gpio_names):
        -            self._dev_gpio[dev_idx] = Device(dev_name, config, hal)
        -            
        -        # Hardcode the proper period
        -        for htr in range(len(self._dev_pwm)):
        -            for ch in range(len(PWM_PINS)):
        -                self.set_period(htr, ch, 10000)
        -
        -    def connect(self):
        -        self._is_connected = True
        -
        -    def disconnect(self):
        -        self._is_connected = False
        -
        -    def reconnect(self):
        -        if self._is_connected:
        -            self.disconnect()
        -        self.connect()
        -
        -    def is_connected(self):
        -        return self._is_connected
        -
        -    def set_enable(self, dev_idx, chnl_idx, enable):
        -        # logger.debug(f"Setting Heater {dev_idx + 1} Channel {chnl_idx + 1} to state: {enable}")
        -        self._dev_pwm[dev_idx].setMultiple([('PWM_NUM', PWM_PINS[chnl_idx]),\
        -                                            ('PWM_ENABLE', enable)])
        -        self._dev_gpio[dev_idx].setMultiple([('GPIO_PIN', LED_PINS[chnl_idx]),\
        -                                             ('GPIO_VALUE', enable)])
        -
        -    def set_duty_cycle(self, dev_idx, chnl_idx, duty_cycle):
        -        # logger.debug(f"Setting duty cycle of Heater {dev_idx + 1} Channel {chnl_idx + 1} to: {duty_cycle}")
        -        self._dev_pwm[dev_idx].setMultiple([('PWM_NUM', PWM_PINS[chnl_idx]), ('PWM_DUTY_CYCLE', duty_cycle)])
        -
        -    def set_period(self, dev_idx, chnl_idx, period):
        -        # logger.debug(f"Setting period of Heater {dev_idx + 1} Channel {chnl_idx + 1} to: {period}")
        -        self._dev_pwm[dev_idx].setMultiple([('PWM_NUM', PWM_PINS[chnl_idx]), ('PWM_PERIOD', period)])
        -
        -    def get_temperature(self, dev_idx):
        -        def twosComp(val, bits):
        -            if (val & (1 << (bits - 1))) != 0:
        -                val = val - (1 << bits)
        -            return val
        -        raw = self._dev_i2c[dev_idx].get('TEMP')
        -        val = ((raw << 8) & 0xFF00) + (raw >> 8)
        -        val = val >> 4
        -        return twosComp(val, 12) * (128/0x7FF)
        -
        -    def get_availability(self):
        -        return self._availablity
        -
        -    def get_voltage(self, dev_idx, chnl_idx):
        -        raw = self._dev_i2c[dev_idx].get('V_{}'.format(chnl_idx))
        -        val = ((raw << 8) & 0xFF00) + (raw >> 8)
        -        val = val >> 4
        -        return val * 0.025
        -
        -    def get_current(self, dev_idx, chnl_idx):
        -        raw = self._dev_i2c[dev_idx].get('I_{}'.format(chnl_idx))
        -        val = ((raw << 8) & 0xFF00) + (raw >> 8)
        -        val = val >> 4
        -        return val * 5e-4
        -
        -    def get_power(self, dev_idx, chnl_idx):
        -        return self.get_voltage(dev_idx, chnl_idx) * \
        -               self.get_current(dev_idx, chnl_idx)
        -
        -    def get_enable(self, dev_idx, chnl_idx):
        -        pwm_enable = self._dev_pwm[dev_idx].getSetMultiple([('PWM_NUM', PWM_PINS[chnl_idx]), ('PWM_ENABLE', None)])
        -        return bool(pwm_enable[0])
        -        
        -    def get_duty_cycle(self, dev_idx, chnl_idx):
        -        pwm_duty_cycle = self._dev_pwm[dev_idx].getSetMultiple([('PWM_NUM', PWM_PINS[chnl_idx]), ('PWM_DUTY_CYCLE', None)])
        -        return int(pwm_duty_cycle[0])
        -
        -def main():
        -    bbb = BeagleboneDeviceInterface()
        -    print(bbb.get_temperature(0))
        -    print(bbb.get_duty_cycle(0, 1))
        -    print(bbb.get_enable(0, 1))
        -
        -if __name__ == '__main__':
        -    main()
        -
        -
        -
        -
        -
        -
        -
        -

        Functions

        -
        -
        -def main() -
        -
        -
        -
        - -Expand source code - -
        def main():
        -    bbb = BeagleboneDeviceInterface()
        -    print(bbb.get_temperature(0))
        -    print(bbb.get_duty_cycle(0, 1))
        -    print(bbb.get_enable(0, 1))
        -
        -
        -
        -
        -
        -

        Classes

        -
        -
        -class BeagleboneCommand -(name, cmd, response=None, wait=None, check=None, description=None, device_method=None) -
        -
        -

        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.

        -
        - -Expand source code - -
        class BeagleboneCommand(ClientServerCommand):
        -    def get_cmd_string(self, *args, **kwargs):
        -        out = super().get_cmd_string(*args, **kwargs)
        -        return out + '\n'
        -
        -

        Ancestors

        - -

        Methods

        -
        -
        -def get_cmd_string(self, *args, **kwargs) -
        -
        -
        -
        - -Expand source code - -
        def get_cmd_string(self, *args, **kwargs):
        -    out = super().get_cmd_string(*args, **kwargs)
        -    return out + '\n'
        -
        -
        -
        -

        Inherited members

        - -
        -
        -class BeagleboneDeviceInterface -
        -
        -
        -
        - -Expand source code - -
        class BeagleboneDeviceInterface:
        -    def __init__(self):
        -
        -        # Perform SRON gssw imports here to avoid issues at other THs
        -        from gssw.config.configclient import ConfigClient
        -        from gssw.common import addLoggingLevel
        -        from gssw.hal.hal import Hal
        -        from gssw.hal.halclient import HalClient
        -        from gssw.lib.device import Device
        -
        -        self._is_connected = False
        -
        -        # InitGssw does something to zmq that breaks cgse logging.
        -        # So instead I copied the essentials here.
        -        configClient = ConfigClient(os.getenv('GSSW_CONFIGURATION_FILE'))
        -        config = configClient.config
        -        addLoggingLevel('data', 15)
        -
        -        with ConfigurationManagerProxy() as cm:
        -            setup = cm.get_setup()
        -
        -        self._availability = setup.gse.beaglebone_heater.availability
        -
        -        hal = Hal()
        -        hal_client = HalClient(hal, config)
        -        hal_client.requestHal()
        -
        -        self.dev_pwm_names = [f'pwm_{dev_idx+1}' for dev_idx, available in enumerate(self._availability) if available]
        -        self.dev_i2c_names = [f'i2c_{dev_idx+1}' for dev_idx, available in enumerate(self._availability)  if available]
        -        self.dev_gpio_names = [f'gpio_{dev_idx+1}' for dev_idx, available in enumerate(self._availability) if available]
        -
        -        self._dev_pwm = [None, None, None, None, None, None]
        -        for dev_idx, dev_name in enumerate(self.dev_pwm_names):
        -            self._dev_pwm[dev_idx] = Device(dev_name, config, hal)
        -
        -        self._dev_i2c = [None, None, None, None, None, None]
        -        for dev_idx, dev_name in enumerate(self.dev_i2c_names):
        -            self._dev_i2c[dev_idx] = Device(dev_name, config, hal)
        -
        -        self._dev_gpio = [None, None, None, None, None, None]
        -        for dev_idx, dev_name in enumerate(self.dev_gpio_names):
        -            self._dev_gpio[dev_idx] = Device(dev_name, config, hal)
        -            
        -        # Hardcode the proper period
        -        for htr in range(len(self._dev_pwm)):
        -            for ch in range(len(PWM_PINS)):
        -                self.set_period(htr, ch, 10000)
        -
        -    def connect(self):
        -        self._is_connected = True
        -
        -    def disconnect(self):
        -        self._is_connected = False
        -
        -    def reconnect(self):
        -        if self._is_connected:
        -            self.disconnect()
        -        self.connect()
        -
        -    def is_connected(self):
        -        return self._is_connected
        -
        -    def set_enable(self, dev_idx, chnl_idx, enable):
        -        # logger.debug(f"Setting Heater {dev_idx + 1} Channel {chnl_idx + 1} to state: {enable}")
        -        self._dev_pwm[dev_idx].setMultiple([('PWM_NUM', PWM_PINS[chnl_idx]),\
        -                                            ('PWM_ENABLE', enable)])
        -        self._dev_gpio[dev_idx].setMultiple([('GPIO_PIN', LED_PINS[chnl_idx]),\
        -                                             ('GPIO_VALUE', enable)])
        -
        -    def set_duty_cycle(self, dev_idx, chnl_idx, duty_cycle):
        -        # logger.debug(f"Setting duty cycle of Heater {dev_idx + 1} Channel {chnl_idx + 1} to: {duty_cycle}")
        -        self._dev_pwm[dev_idx].setMultiple([('PWM_NUM', PWM_PINS[chnl_idx]), ('PWM_DUTY_CYCLE', duty_cycle)])
        -
        -    def set_period(self, dev_idx, chnl_idx, period):
        -        # logger.debug(f"Setting period of Heater {dev_idx + 1} Channel {chnl_idx + 1} to: {period}")
        -        self._dev_pwm[dev_idx].setMultiple([('PWM_NUM', PWM_PINS[chnl_idx]), ('PWM_PERIOD', period)])
        -
        -    def get_temperature(self, dev_idx):
        -        def twosComp(val, bits):
        -            if (val & (1 << (bits - 1))) != 0:
        -                val = val - (1 << bits)
        -            return val
        -        raw = self._dev_i2c[dev_idx].get('TEMP')
        -        val = ((raw << 8) & 0xFF00) + (raw >> 8)
        -        val = val >> 4
        -        return twosComp(val, 12) * (128/0x7FF)
        -
        -    def get_availability(self):
        -        return self._availablity
        -
        -    def get_voltage(self, dev_idx, chnl_idx):
        -        raw = self._dev_i2c[dev_idx].get('V_{}'.format(chnl_idx))
        -        val = ((raw << 8) & 0xFF00) + (raw >> 8)
        -        val = val >> 4
        -        return val * 0.025
        -
        -    def get_current(self, dev_idx, chnl_idx):
        -        raw = self._dev_i2c[dev_idx].get('I_{}'.format(chnl_idx))
        -        val = ((raw << 8) & 0xFF00) + (raw >> 8)
        -        val = val >> 4
        -        return val * 5e-4
        -
        -    def get_power(self, dev_idx, chnl_idx):
        -        return self.get_voltage(dev_idx, chnl_idx) * \
        -               self.get_current(dev_idx, chnl_idx)
        -
        -    def get_enable(self, dev_idx, chnl_idx):
        -        pwm_enable = self._dev_pwm[dev_idx].getSetMultiple([('PWM_NUM', PWM_PINS[chnl_idx]), ('PWM_ENABLE', None)])
        -        return bool(pwm_enable[0])
        -        
        -    def get_duty_cycle(self, dev_idx, chnl_idx):
        -        pwm_duty_cycle = self._dev_pwm[dev_idx].getSetMultiple([('PWM_NUM', PWM_PINS[chnl_idx]), ('PWM_DUTY_CYCLE', None)])
        -        return int(pwm_duty_cycle[0])
        -
        -

        Methods

        -
        -
        -def connect(self) -
        -
        -
        -
        - -Expand source code - -
        def connect(self):
        -    self._is_connected = True
        -
        -
        -
        -def disconnect(self) -
        -
        -
        -
        - -Expand source code - -
        def disconnect(self):
        -    self._is_connected = False
        -
        -
        -
        -def get_availability(self) -
        -
        -
        -
        - -Expand source code - -
        def get_availability(self):
        -    return self._availablity
        -
        -
        -
        -def get_current(self, dev_idx, chnl_idx) -
        -
        -
        -
        - -Expand source code - -
        def get_current(self, dev_idx, chnl_idx):
        -    raw = self._dev_i2c[dev_idx].get('I_{}'.format(chnl_idx))
        -    val = ((raw << 8) & 0xFF00) + (raw >> 8)
        -    val = val >> 4
        -    return val * 5e-4
        -
        -
        -
        -def get_duty_cycle(self, dev_idx, chnl_idx) -
        -
        -
        -
        - -Expand source code - -
        def get_duty_cycle(self, dev_idx, chnl_idx):
        -    pwm_duty_cycle = self._dev_pwm[dev_idx].getSetMultiple([('PWM_NUM', PWM_PINS[chnl_idx]), ('PWM_DUTY_CYCLE', None)])
        -    return int(pwm_duty_cycle[0])
        -
        -
        -
        -def get_enable(self, dev_idx, chnl_idx) -
        -
        -
        -
        - -Expand source code - -
        def get_enable(self, dev_idx, chnl_idx):
        -    pwm_enable = self._dev_pwm[dev_idx].getSetMultiple([('PWM_NUM', PWM_PINS[chnl_idx]), ('PWM_ENABLE', None)])
        -    return bool(pwm_enable[0])
        -
        -
        -
        -def get_power(self, dev_idx, chnl_idx) -
        -
        -
        -
        - -Expand source code - -
        def get_power(self, dev_idx, chnl_idx):
        -    return self.get_voltage(dev_idx, chnl_idx) * \
        -           self.get_current(dev_idx, chnl_idx)
        -
        -
        -
        -def get_temperature(self, dev_idx) -
        -
        -
        -
        - -Expand source code - -
        def get_temperature(self, dev_idx):
        -    def twosComp(val, bits):
        -        if (val & (1 << (bits - 1))) != 0:
        -            val = val - (1 << bits)
        -        return val
        -    raw = self._dev_i2c[dev_idx].get('TEMP')
        -    val = ((raw << 8) & 0xFF00) + (raw >> 8)
        -    val = val >> 4
        -    return twosComp(val, 12) * (128/0x7FF)
        -
        -
        -
        -def get_voltage(self, dev_idx, chnl_idx) -
        -
        -
        -
        - -Expand source code - -
        def get_voltage(self, dev_idx, chnl_idx):
        -    raw = self._dev_i2c[dev_idx].get('V_{}'.format(chnl_idx))
        -    val = ((raw << 8) & 0xFF00) + (raw >> 8)
        -    val = val >> 4
        -    return val * 0.025
        -
        -
        -
        -def is_connected(self) -
        -
        -
        -
        - -Expand source code - -
        def is_connected(self):
        -    return self._is_connected
        -
        -
        -
        -def reconnect(self) -
        -
        -
        -
        - -Expand source code - -
        def reconnect(self):
        -    if self._is_connected:
        -        self.disconnect()
        -    self.connect()
        -
        -
        -
        -def set_duty_cycle(self, dev_idx, chnl_idx, duty_cycle) -
        -
        -
        -
        - -Expand source code - -
        def set_duty_cycle(self, dev_idx, chnl_idx, duty_cycle):
        -    # logger.debug(f"Setting duty cycle of Heater {dev_idx + 1} Channel {chnl_idx + 1} to: {duty_cycle}")
        -    self._dev_pwm[dev_idx].setMultiple([('PWM_NUM', PWM_PINS[chnl_idx]), ('PWM_DUTY_CYCLE', duty_cycle)])
        -
        -
        -
        -def set_enable(self, dev_idx, chnl_idx, enable) -
        -
        -
        -
        - -Expand source code - -
        def set_enable(self, dev_idx, chnl_idx, enable):
        -    # logger.debug(f"Setting Heater {dev_idx + 1} Channel {chnl_idx + 1} to state: {enable}")
        -    self._dev_pwm[dev_idx].setMultiple([('PWM_NUM', PWM_PINS[chnl_idx]),\
        -                                        ('PWM_ENABLE', enable)])
        -    self._dev_gpio[dev_idx].setMultiple([('GPIO_PIN', LED_PINS[chnl_idx]),\
        -                                         ('GPIO_VALUE', enable)])
        -
        -
        -
        -def set_period(self, dev_idx, chnl_idx, period) -
        -
        -
        -
        - -Expand source code - -
        def set_period(self, dev_idx, chnl_idx, period):
        -    # logger.debug(f"Setting period of Heater {dev_idx + 1} Channel {chnl_idx + 1} to: {period}")
        -    self._dev_pwm[dev_idx].setMultiple([('PWM_NUM', PWM_PINS[chnl_idx]), ('PWM_PERIOD', period)])
        -
        -
        -
        -
        -
        -
        -
        - -
        - - - \ No newline at end of file diff --git a/docs/api/egse/tempcontrol/beaglebone/beaglebone_ui.html b/docs/api/egse/tempcontrol/beaglebone/beaglebone_ui.html deleted file mode 100644 index af6db24..0000000 --- a/docs/api/egse/tempcontrol/beaglebone/beaglebone_ui.html +++ /dev/null @@ -1,2231 +0,0 @@ - - - - - - -egse.tempcontrol.beaglebone.beaglebone_ui API documentation - - - - - - - - - - - -
        -
        -
        -

        Module egse.tempcontrol.beaglebone.beaglebone_ui

        -
        -
        -
        - -Expand source code - -
        import argparse
        -import logging
        -import pickle
        -from pathlib import Path
        -from re import S
        -import sys
        -import zmq
        -from zmq import ZMQError
        -
        -from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot, QTimer, QEvent, QSize, QLockFile
        -from PyQt5.QtGui import QCloseEvent
        -from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel, QFrame, QHBoxLayout, QVBoxLayout, QGroupBox, QGridLayout, \
        -    QAction, QWidget, QLineEdit,  QPushButton, QSizePolicy, QSpacerItem, QSpinBox, QCheckBox, QMessageBox
        -
        -from egse.gui.led import LED
        -from egse.gui.buttons import ToggleButton
        -
        -from egse.tempcontrol.beaglebone.beaglebone import BeagleboneProxy, BeagleboneSimulator
        -from egse.tempcontrol.beaglebone.beaglebone import CTRL_SETTINGS as HEATER_CTRL_SETTINGS
        -from egse.tempcontrol.spid.spid import PidProxy, PidSimulator
        -from egse.tempcontrol.spid.spid import CTRL_SETTINGS as PID_CTRL_SETTINGS
        -
        -from egse.setup import get_setup
        -from egse.gui.buttons import ToggleButton
        -from egse.observer import Observer, Observable
        -from egse.settings import Settings
        -from egse.setup import Setup
        -from egse.zmq_ser import connect_address
        -from egse.resource import get_resource
        -
        -logger = logging.getLogger("BBB heater UI")
        -
        -class HeaterMonitoringWorker(QObject):
        -    """Worker for monitoring the state and duty cycle of the Beaglebone Black Heaters
        -
        -    The worker will send a signal when a heater state changes or 
        -        when the duty cycle changes
        -
        -    """
        -
        -    heater_enabled_status_signal        = pyqtSignal(list)
        -    heater_duty_cycle_status_signal                   = pyqtSignal(list)
        -    
        -    def __init__(self):
        -        """ Initialisation of a monitoring worker.
        -        
        -        This worker keeps an eye on the monitoring port of the Beaglebone Heater. When a change in
        -            Relevant information occurs, a signalw ill be emitted. These signals will be used to update the GUI
        -        """
        -        
        -        
        -        super(HeaterMonitoringWorker, self).__init__()
        -        
        -        self.active = False
        -        self.just_reconnected = True
        -        
        -        self.monitoring_socket = None
        -        self.is_socket_connected = True
        -        self.monitoring_timeout = 0.5
        -        
        -        self.connect_socket()
        -        
        -        # Keep track of the heater status, so we only have to send a signal when the state has changed
        -        
        -        self.previous_heater_status_signal = {}
        -        self.previous_heater_duty_cycle    = {}
        -        
        -    def connect_socket(self):
        -        """ Create a socket and connect to the monitoring port.
        -        """
        -        
        -
        -        try:
        -            transport   = HEATER_CTRL_SETTINGS.PROTOCOL
        -            hostname    = HEATER_CTRL_SETTINGS.HOSTNAME
        -            
        -            monitoring_port = HEATER_CTRL_SETTINGS.MONITORING_PORT
        -            monitoring_address = connect_address(transport, hostname, monitoring_port)
        -            
        -            self.monitoring_socket = zmq.Context().socket(zmq.SUB)
        -            self.monitoring_socket.connect(monitoring_address)
        -            self.monitoring_socket.setsockopt_string(zmq.SUBSCRIBE, "")
        -            
        -            self.monitoring_timeout = 0.5
        -            
        -            self.is_socket_connected = True
        -            
        -        except:
        -            self.is_socket_connected = False
        -            
        -    def stop(self):
        -        
        -        """ Stop the monitoring worker.
        -
        -        The monitoring socket is disconnected from the monitoring port and is then closed immediately.
        -        """
        -        
        -        self.monitoring_socket.close()
        -        self.is_socket_connected = False
        -        
        -        self.active = False
        -        
        -    def start_process(self):
        -        """Start updated the Beaglebone status"""
        -        self.run()
        -        
        -    @pyqtSlot()
        -    def run(self):
        -        """Keep on checkin whether the Beaglebone state has changed
        -        
        -        If the beaglebone status has changed, update it in the GUI
        -
        -        Raises:
        -            Exception: ZMQ Error
        -        """
        -        
        -        self.active = True
        -        while self.is_socket_connected and self.active:
        -            
        -            try:
        -                socket_list, _, exc_list = zmq.select([self.monitoring_socket], [], [], timeout=self.monitoring_timeout)
        -
        -                if self.monitoring_socket in socket_list:
        -                    try:
        -                        pickle_string = self.monitoring_socket.recv()
        -                    except Exception:
        -                        raise Exception
        -
        -                    monitoring_info = pickle.loads(pickle_string)
        -                    heater_enabled      = monitoring_info['Enabled']
        -                    heater_duty_cycle   = monitoring_info['duty_cycle']
        -
        -                    self.heater_enabled_status_signal.emit(heater_enabled)
        -                    
        -                    if heater_duty_cycle != self.previous_heater_duty_cycle:
        -                        self.heater_duty_cycle_status_signal.emit(heater_duty_cycle)
        -                    
        -                    self.previous_heater_duty_cycle = heater_duty_cycle
        -                    
        -                    
        -            except ZMQError as exc:
        -                raise exc
        - 
        -class PidMonitoringWorker(QObject):
        -    """Worker for monitoring the state of the individual PID channels
        -
        -   The worker will send a signal when the state of a PID channel has changed
        -
        -    """
        -    
        -    pid_control_server_status_signal = pyqtSignal(bool)
        -    pid_enabled_status_signal        = pyqtSignal(dict)
        -    
        -    def __init__(self):
        -        super(PidMonitoringWorker, self).__init__()
        -        
        -        self.active = False
        -        self.just_reconnected = True
        -        
        -        self.enabled = False
        -        self.pid_enabled = []
        -        
        -        self.monitoring_socket = None
        -        self.is_socket_connected = True
        -        self.monitoring_timeout = 0.5
        -        
        -        self.connect_socket()
        -        
        -        self.previous_control_server_status_signal = ()
        -        self.previous_pid_enabled_status_signal = {}
        -
        -
        -    def connect_socket(self):
        -        try:
        -            transport   = PID_CTRL_SETTINGS.PROTOCOL
        -            hostname    = PID_CTRL_SETTINGS.HOSTNAME
        -            
        -            monitoring_port = PID_CTRL_SETTINGS.MONITORING_PORT
        -            monitoring_address = connect_address(transport, hostname, monitoring_port)
        -            self.monitoring_socket = zmq.Context().socket(zmq.SUB)
        -            self.monitoring_socket.connect(monitoring_address)
        -            self.monitoring_socket.setsockopt_string(zmq.SUBSCRIBE, "")
        -            
        -            
        -            self.monitoring_timeout = 0.5
        -            
        -            self.is_socket_connected = True
        -            
        -        except:
        -            self.is_socket_connected = False
        -            
        -    def stop(self):
        -        self.monitoring_socket.close()
        -        self.is_socket_connected = False
        -        
        -        self.active = False
        -        
        -    def start_process(self):
        -        self.run()
        -        
        -    @pyqtSlot()
        -    def run(self):
        -        self.active = True
        -        while self.is_socket_connected and self.active:
        -            
        -            try:
        -                socket_list, _, exc_list = zmq.select([self.monitoring_socket], [], [], timeout=self.monitoring_timeout)
        -                if self.monitoring_socket in socket_list:
        -                    self.enabled = True
        -                    pickle_string = self.monitoring_socket.recv()
        -
        -                    monitoring_info = pickle.loads(pickle_string)
        -                    self.pid_enabled      = monitoring_info['Enabled']
        -                    
        -                    if self.previous_pid_enabled_status_signal != self.pid_enabled:
        -                        self.pid_enabled_status_signal.emit(self.pid_enabled)
        -                    
        -                if self.previous_control_server_status_signal != self.enabled:
        -                    self.pid_control_server_status_signal.emit(self.enabled)
        -                    
        -                self.previous_control_server_status_signal = self.enabled
        -                self.previous_pid_enabled_status_signal = self.pid_enabled
        -            except ZMQError as exc:
        -                raise exc
        -            
        -            
        -class ChannelGroupbox(QGroupBox):
        -    def __init__(self, parent, name):
        -        super().__init__(parent=parent)
        -        self.setObjectName(name)
        -        self.resize(250, 80)
        -        self.setMinimumSize(QSize(250, 80))
        -        self.setMaximumSize(QSize(300, 100))
        -        self.verticalLayout = QVBoxLayout(self)
        -        self.verticalLayout.setObjectName("verticalLayout")
        -        self.horizontalLayout = QHBoxLayout()
        -        self.horizontalLayout.setObjectName("horizontalLayout")
        -        self.label = QLabel(self)
        -        self.label.setObjectName("label")
        -        self.horizontalLayout.addWidget(self.label)
        -        spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        -        self.horizontalLayout.addItem(spacerItem)
        -        self.pushButton = QPushButton(self)
        -        self.pushButton.setObjectName("pushButton")
        -        self.pushButton.setText("Stop PID")
        -        self.pushButton.setMaximumSize(QSize(150, 25))
        -        self.horizontalLayout.addWidget(self.pushButton)
        -        self.verticalLayout.addLayout(self.horizontalLayout)
        -        self.horizontalLayout_2 = QHBoxLayout()
        -        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        -        self.checkBox = ToggleButton(
        -            name="Turn On\Off Heater Channel",
        -            status_tip=f"Heater {parent.objectName()} Channel {self.objectName()} On\Off",
        -            selected=get_resource(":/icons/switch-on.svg"),
        -            not_selected=get_resource(":/icons/switch-off.svg"),
        -        )
        -        self.horizontalLayout_2.addWidget(self.checkBox)
        -        spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        -        self.horizontalLayout_2.addItem(spacerItem1)
        -        self.label_2 = QLabel(self)
        -        self.label_2.setObjectName("label_2")
        -        self.label_2.setText("P")
        -        self.horizontalLayout_2.addWidget(self.label_2)
        -        self.spinBox = QSpinBox(self)
        -        self.spinBox.setMaximumSize(QSize(150, 16777215))
        -        self.spinBox.setObjectName("spinBox")
        -        self.spinBox.setSuffix(" %")
        -        self.horizontalLayout_2.addWidget(self.spinBox)
        -        self.pushButton_2 = QPushButton(self)
        -        self.pushButton_2.setMaximumSize(QSize(150, 25))
        -        self.pushButton_2.setObjectName("pushButton_2")
        -        self.pushButton_2.setText("Set")
        -        self.horizontalLayout_2.addWidget(self.pushButton_2)
        -        self.verticalLayout.addLayout(self.horizontalLayout_2)
        -    
        -class HeaterGroupbox(QGroupBox):
        -    def __init__(self, parent, name):
        -        super().__init__(parent=parent)
        -        self.setObjectName(name)
        -        self.resize(280, 407)
        -        self.setMaximumSize(QSize(280, 410))
        -        self.verticalLayout = QVBoxLayout(self)
        -        self.verticalLayout.setObjectName("verticalLayout")
        -        self.horizontalLayout = QHBoxLayout()
        -        self.horizontalLayout.setObjectName("horizontalLayout")
        -        self.label = QLabel(self)
        -        self.label.setObjectName("label")
        -        self.horizontalLayout.addWidget(self.label)
        -        spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        -        self.horizontalLayout.addItem(spacerItem)
        -        self.widget = LED(self)
        -        self.widget.setMinimumSize(QSize(25, 25))
        -        self.widget.setMaximumSize(QSize(25, 25))
        -        self.widget.setObjectName("widget")
        -        self.horizontalLayout.addWidget(self.widget)
        -        self.verticalLayout.addLayout(self.horizontalLayout)
        -        self.groupBox = ChannelGroupbox(self, "Channel A")
        -        self.groupBox.setTitle("")
        -        self.verticalLayout.addWidget(self.groupBox)
        -        self.groupBox_2 = ChannelGroupbox(self, "Channel B")
        -        self.groupBox_2.setTitle("")
        -        self.verticalLayout.addWidget(self.groupBox_2)
        -        self.groupBox_3 = ChannelGroupbox(self, "Channel C")
        -        self.groupBox_3.setTitle("")
        -        self.verticalLayout.addWidget(self.groupBox_3)
        -        self.groupBox_4 = ChannelGroupbox(self, "Channel D")
        -        self.groupBox_4.setTitle("")
        -        self.verticalLayout.addWidget(self.groupBox_4)   
        -        
        -class HeaterMainWindow(QMainWindow, Observable):
        -    def __init__(self, parent=None):
        -        super().__init__(parent=parent)
        -        self.setWindowTitle("BeagleBone Black Heater Controller")
        -        
        -        self.setMaximumSize(QSize(1700, 420))
        -        
        -        self.horizontalLayout = QHBoxLayout()
        -        self.centralWidget = QWidget()
        -        self.centralWidget.setLayout(self.horizontalLayout)
        -        
        -        self.setCentralWidget(self.centralWidget)
        -        
        -        self.initUI()
        -        
        -        self.heater_monitoring_thread = QThread(self)
        -        self.heater_monitoring_worker = HeaterMonitoringWorker()
        -        self.heater_monitoring_worker.moveToThread(self.heater_monitoring_thread)
        -        
        -        self.pid_monitoring_thread = QThread(self)
        -        self.pid_monitoring_worker = PidMonitoringWorker()
        -        self.pid_monitoring_worker.moveToThread(self.pid_monitoring_thread)
        -
        -        self.heater_monitoring_worker.heater_enabled_status_signal.connect(self.on_heater_enabled_status_signal)
        -        self.heater_monitoring_worker.heater_duty_cycle_status_signal.connect(self.on_heater_duty_cycle_status_signal)
        -        
        -        self.pid_monitoring_worker.pid_enabled_status_signal.connect(self.on_pid_enabled_status_signal)
        -
        -        self.heater_monitoring_thread.started.connect(self.heater_monitoring_worker.start_process)
        -        self.heater_monitoring_thread.start()
        -        
        -        self.pid_monitoring_thread.started.connect(self.pid_monitoring_worker.start_process)
        -        self.pid_monitoring_thread.start()
        -        
        -    def initUI(self):
        -        setup = get_setup()
        -        
        -        self.htr_availability =  [True for available in setup.gse.beaglebone_heater.availability if available]
        -        self.heaters = [None] * len(self.htr_availability)
        -        self.channels = [None] * len(self.htr_availability)
        -        pid_configuration                = setup.gse.spid.configuration.heaters
        -        self.pids = [channel for heater in pid_configuration.values() for channel in heater]
        -        
        -        for idx, heater in enumerate(self.heaters):
        -            heater_name = "Heater {}".format(idx + 1)
        -            avail = self.htr_availability[idx]
        -            self.heaters[idx] = HeaterGroupbox(self, heater_name)
        -            self.heaters[idx].widget.set_color(1 if avail else 0)
        -            self.heaters[idx].label.setText(heater_name)
        -            self.horizontalLayout.addWidget(self.heaters[idx])
        -            
        -            self.channels[idx] = [None] * 4
        -            
        -            for ch_idx, channel in enumerate(self.channels[idx]):
        -                channel_name = "Channel A" if ch_idx == 0 else \
        -                                "Channel B" if ch_idx == 1 else \
        -                                "Channel C" if ch_idx == 2 else \
        -                                "Channel D"    
        -                self.channels[idx][ch_idx]  = self.heaters[idx].findChild(QGroupBox, channel_name)
        -                self.channels[idx][ch_idx].pushButton.hide()
        -                # channel.setObjectName(heater_name + channel_name)
        -                self.channels[idx][ch_idx].label.setText(channel_name)
        -                self.channels[idx][ch_idx].pushButton_2.clicked.connect(self.set_duty_cycle)
        -                self.channels[idx][ch_idx].checkBox.clicked.connect(self.set_enable)
        -                self.channels[idx][ch_idx].pushButton.clicked.connect(self.turn_off_pid)
        -        
        -    def on_heater_enabled_status_signal(self, monitoring_info: dict):
        -        for htr_idx, heater in enumerate(self.heaters):
        -            for ch_idx, channel in enumerate(self.channels[htr_idx]):
        -                channel.checkBox.set_selected(monitoring_info[htr_idx][ch_idx])
        -    
        -    def on_heater_duty_cycle_status_signal(self, monitoring_info: dict):
        -        for htr_idx, heater in enumerate(self.heaters):
        -            for ch_idx, channel in enumerate(self.channels[htr_idx]):
        -                channel.spinBox.setValue(int(monitoring_info[htr_idx][ch_idx] / 100))
        -    
        -    def on_pid_enabled_status_signal(self, monitoring_info: dict):
        -        for heater_channel, enabled in monitoring_info.items():
        -            command = heater_channel.split("_")
        -            htr = int(command[0])
        -            ch = int(command[1])
        -            if enabled:
        -
        -                self.channels[htr][ch].pushButton.show()
        -                self.channels[htr][ch].spinBox.setReadOnly(True)
        -                self.channels[htr][ch].setStyleSheet("""QGroupBox {border: 1px solid red;}""")
        -            
        -            else:
        -                self.channels[htr][ch].pushButton.hide()
        -                self.channels[htr][ch].spinBox.setReadOnly(False)
        -                self.channels[htr][ch].setStyleSheet('fusion') 
        -    
        -    def set_duty_cycle(self):
        -        htr = self.sender().parent().parent().objectName().split(" ")[1]
        -        channel = self.sender().parent().objectName().split(" ")[1]
        -        value = self.sender().parent().spinBox.value()
        -        duty_cycle = int(value * 100)
        -
        -        try:
        -            self.actionObservers({"set_duty_cycle": [int(htr)-1, int(ord(channel)-65), duty_cycle]})
        -        except Exception as e:
        -            warning_popup("set_duty_cycle : Heater {} Channel {} duty_cycle {}".format(
        -                htr, channel, duty_cycle
        -            ), e)
        -    
        -    
        -    def turn_off_pid(self):
        -        htr = self.sender().parent().parent().objectName().split(" ")[1]
        -        channel = self.sender().parent().objectName().split(" ")[1]
        -        
        -        for idx, pid in enumerate(self.pids):
        -            if (pid[3] is htr) and (pid[4] is channel):
        -                try:
        -                    self.actionObservers({"set_pid_disable": [int(idx)]}) if (pid[3] is htr and pid[4] is channel) else None
        -                except Exception as e:
        -                    warning_popup("set_pid_disable : {}".format(idx), e)
        -    
        -    def set_enable(self):
        -        htr = self.sender().parent().parent().objectName().split(" ")[1]
        -        channel = self.sender().parent().objectName().split(" ")[1]
        -        value = self.sender().is_selected()
        -        self.sender().set_selected(on=value)
        -        # logger.info("{} {} {}".format(int(htr), int(channel), bool(value)))
        -        try:
        -            self.actionObservers({"set_enable" : [int(htr)-1, int(ord(channel)-65), bool(value)]})
        -        except Exception as e:
        -            warning_popup('set_enable : Heater {} Channel {} to {}'.format(
        -                htr, channel, "Enabled" if value else "Disabled"
        -            ), e)
        -
        -
        -def warning_popup(command, error):
        -    msgBox = QMessageBox()
        -    msgBox.setWindowTitle("OGSE error occured")
        -    msgBox.setIcon(QMessageBox.Warning)
        -    msgBox.setText(f"An error occured while executing: {command}")
        -    msgBox.setInformativeText(f"{error}")
        -    msgBox.setStandardButtons(QMessageBox.Ok)
        -    msgBox.exec_()
        -        
        -class HeaterUIModel:
        -    def __init__(self, mode):
        -            self.mode = mode
        -
        -            if self.mode == 'proxy':
        -                try:
        -                    self.heater = BeagleboneProxy()
        -                except Exception as exc:
        -                    raise exc
        -
        -            elif self.mode == 'simulator':
        -                self.heater = BeagleboneSimulator()
        -            else:
        -                raise ValueError(f'Unknown type of Heater implementation passed into the model')
        -            
        -            if self.heater is not None:
        -                logger.debug(f'Heater Controller initialized as {self.heater.__class__.__name__}')
        -
        -
        -    def set_enable(self, htr_idx, ch_idx, enable):
        -        with BeagleboneProxy() as heater:
        -            heater.set_enable(htr_idx, ch_idx, enable)
        -        
        -    def set_duty_cycle(self, htr_idx, ch_idx, duty_cycle):
        -        with BeagleboneProxy() as heater:
        -            heater.set_duty_cycle(htr_idx, ch_idx, duty_cycle)
        -
        -    def set_pid_enable(self, ch):
        -        try:
        -            with PidProxy() as pid:
        -                pid.enable(ch)
        -        except:
        -            logger.info("PID Control Server could not be found")
        -        
        -    def set_pid_disable(self, ch):
        -        logger.info(f"PID channel {ch} has been disabled")
        -        try:
        -            with PidProxy() as pid:
        -                pid.disable(ch)
        -        except:
        -            logger.info("PID Control Server could not be found")
        -    
        -    def has_commands(self):
        -        if self.mode == "proxy":
        -            with BeagleboneProxy() as heater:
        -                return heater.has_commands()
        -
        -        return True
        -
        -    def load_commands(self):
        -        if self.mode == "proxy":
        -            with BeagleboneProxy() as heater:
        -                with PidProxy() as pid:
        -                    pid.load_commands()
        -                    heater.load_commands()
        -
        -    def is_simulator(self):
        -        with BeagleboneProxy() as heater:
        -            return heater.is_simulator()
        -
        -    def is_connected(self):
        -        if self.mode == "proxy":
        -            with BeagleboneProxy() as heater:
        -                return heater.is_cs_connected()
        -
        -        else:
        -
        -            return self.heater.is_connected()
        -
        -    def reconnect(self):
        -        if self.mode == "proxy":
        -            with BeagleboneProxy() as heater:
        -
        -                heater.reconnect_cs()
        -
        -                return heater.is_cs_connected()
        -
        -        else:
        -
        -            self.heater.reconnect()
        -
        -            return self.heater.is_connected()
        -
        -        return False
        -
        -    def disconnect(self):
        -        if self.mode == "proxy":
        -
        -            self.mode.disconnect_cs()
        -
        -class HeaterUIController(Observer):
        -    def __init__(self, model: HeaterUIModel, view: HeaterMainWindow):
        -        self.model  = model
        -        self.view   = view
        -        self.view.addObserver(self)
        - 
        -    def update(self, changed_object):
        -
        -        text = changed_object.text()
        -
        -        if text == "Reconnect":
        -
        -            if changed_object.isChecked():
        -
        -                logger.debug("Reconnecting the Heater model.")
        -
        -                if self.model.reconnect():
        -
        -                    self.view.set_connection_state(True)
        -
        -                    if not self.model.has_commands():
        -
        -                        self.model.load_commands()
        -
        -                else:
        -                    self.view.reconnect_action.setChecked(False)
        -            else:
        -
        -                logger.debug("Disconnecting the Heater model.")
        -                self.model.disconnect()
        -                self.view.set_connection_state(False)
        -
        -            return
        - 
        -    
        -    def do(self, actions):
        -        for action, value in actions.items():
        -            logger.debug(f"do {action} with {value}")
        -            if action == "set_duty_cycle":
        -                htr_idx     = value[0]
        -                ch_idx      = value[1]
        -                duty_cycle  = value[2]
        -                logger.debug(f"do: set_duty_cycle({htr_idx}, {ch_idx}, {duty_cycle})")
        -                self.model.set_duty_cycle(htr_idx, ch_idx, duty_cycle)
        -
        -                
        -            if action == "set_enable":
        -                htr_idx     = value[0]
        -                ch_idx      = value[1]
        -                state  = value[2]
        -                logger.debug(f"do: set_enable({htr_idx}, {ch_idx}, {state})")
        -                self.model.set_enable(htr_idx, ch_idx, state)
        -                
        -            if action == "set_pid_enable":
        -                ch          = value[0]
        -                logger.debug(f"do: set_pid_enable({ch}")
        -                self.model.set_pid_enable(ch)
        -                
        -            if action == "set_pid_disable":
        -                ch          = value[0]
        -                logger.info(f"do: set_pid_disable({ch})")
        -                self.model.set_pid_disable(ch)
        -    
        -def parse_arguments():
        -    """
        -    Prepare the arguments that are specific for this application.
        -    """
        -    parser = argparse.ArgumentParser()
        -    parser.add_argument(
        -        "--type",
        -        dest="type",
        -        action="store",
        -        choices={"proxy", "simulator", "crio"},
        -        help="Specify Beaglebone Black Heater implementation you want to connect to.",
        -        default="proxy",
        -    )    
        -    args = parser.parse_args()
        -    return args
        -
        -
        -def main():
        -    lock_file = QLockFile(str(Path("~/tempcontrol_beaglebone_ui.app.lock").expanduser()))
        -
        -    args = list(sys.argv)
        -    app = QApplication(args)
        -    # app.setWindowIcon(QIcon(str(get_resource(":/icons/temperature-control.svg"))))
        -
        -    if lock_file.tryLock(100):
        -        args = parse_arguments()
        -
        -        if args.type == 'proxy':
        -            try:
        -                proxy = BeagleboneProxy()
        -            except Exception:
        -                logger.info("Beaglebone Black Heater Control Server is not running")
        -
        -        view = HeaterMainWindow()
        -        model = HeaterUIModel(args.type)
        -        controller = HeaterUIController(model, view)
        -
        -        view.show()
        -        return app.exec_()
        -    else:
        -        error_message = QMessageBox()
        -        error_message.setIcon(QMessageBox.Warning)
        -        error_message.setWindowTitle("Error")
        -        error_message.setText("The Beaglebone tempcontrol GUI application is already running!")
        -        error_message.setStandardButtons(QMessageBox.Ok)
        -
        -        return error_message.exec()
        -
        -    
        -if __name__ == "__main__":
        -    main()
        -
        -
        -
        -
        -
        -
        -
        -

        Functions

        -
        -
        -def main() -
        -
        -
        -
        - -Expand source code - -
        def main():
        -    lock_file = QLockFile(str(Path("~/tempcontrol_beaglebone_ui.app.lock").expanduser()))
        -
        -    args = list(sys.argv)
        -    app = QApplication(args)
        -    # app.setWindowIcon(QIcon(str(get_resource(":/icons/temperature-control.svg"))))
        -
        -    if lock_file.tryLock(100):
        -        args = parse_arguments()
        -
        -        if args.type == 'proxy':
        -            try:
        -                proxy = BeagleboneProxy()
        -            except Exception:
        -                logger.info("Beaglebone Black Heater Control Server is not running")
        -
        -        view = HeaterMainWindow()
        -        model = HeaterUIModel(args.type)
        -        controller = HeaterUIController(model, view)
        -
        -        view.show()
        -        return app.exec_()
        -    else:
        -        error_message = QMessageBox()
        -        error_message.setIcon(QMessageBox.Warning)
        -        error_message.setWindowTitle("Error")
        -        error_message.setText("The Beaglebone tempcontrol GUI application is already running!")
        -        error_message.setStandardButtons(QMessageBox.Ok)
        -
        -        return error_message.exec()
        -
        -
        -
        -def parse_arguments() -
        -
        -

        Prepare the arguments that are specific for this application.

        -
        - -Expand source code - -
        def parse_arguments():
        -    """
        -    Prepare the arguments that are specific for this application.
        -    """
        -    parser = argparse.ArgumentParser()
        -    parser.add_argument(
        -        "--type",
        -        dest="type",
        -        action="store",
        -        choices={"proxy", "simulator", "crio"},
        -        help="Specify Beaglebone Black Heater implementation you want to connect to.",
        -        default="proxy",
        -    )    
        -    args = parser.parse_args()
        -    return args
        -
        -
        -
        -def warning_popup(command, error) -
        -
        -
        -
        - -Expand source code - -
        def warning_popup(command, error):
        -    msgBox = QMessageBox()
        -    msgBox.setWindowTitle("OGSE error occured")
        -    msgBox.setIcon(QMessageBox.Warning)
        -    msgBox.setText(f"An error occured while executing: {command}")
        -    msgBox.setInformativeText(f"{error}")
        -    msgBox.setStandardButtons(QMessageBox.Ok)
        -    msgBox.exec_()
        -
        -
        -
        -
        -
        -

        Classes

        -
        -
        -class ChannelGroupbox -(parent, name) -
        -
        -

        QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

        -
        - -Expand source code - -
        class ChannelGroupbox(QGroupBox):
        -    def __init__(self, parent, name):
        -        super().__init__(parent=parent)
        -        self.setObjectName(name)
        -        self.resize(250, 80)
        -        self.setMinimumSize(QSize(250, 80))
        -        self.setMaximumSize(QSize(300, 100))
        -        self.verticalLayout = QVBoxLayout(self)
        -        self.verticalLayout.setObjectName("verticalLayout")
        -        self.horizontalLayout = QHBoxLayout()
        -        self.horizontalLayout.setObjectName("horizontalLayout")
        -        self.label = QLabel(self)
        -        self.label.setObjectName("label")
        -        self.horizontalLayout.addWidget(self.label)
        -        spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        -        self.horizontalLayout.addItem(spacerItem)
        -        self.pushButton = QPushButton(self)
        -        self.pushButton.setObjectName("pushButton")
        -        self.pushButton.setText("Stop PID")
        -        self.pushButton.setMaximumSize(QSize(150, 25))
        -        self.horizontalLayout.addWidget(self.pushButton)
        -        self.verticalLayout.addLayout(self.horizontalLayout)
        -        self.horizontalLayout_2 = QHBoxLayout()
        -        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        -        self.checkBox = ToggleButton(
        -            name="Turn On\Off Heater Channel",
        -            status_tip=f"Heater {parent.objectName()} Channel {self.objectName()} On\Off",
        -            selected=get_resource(":/icons/switch-on.svg"),
        -            not_selected=get_resource(":/icons/switch-off.svg"),
        -        )
        -        self.horizontalLayout_2.addWidget(self.checkBox)
        -        spacerItem1 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        -        self.horizontalLayout_2.addItem(spacerItem1)
        -        self.label_2 = QLabel(self)
        -        self.label_2.setObjectName("label_2")
        -        self.label_2.setText("P")
        -        self.horizontalLayout_2.addWidget(self.label_2)
        -        self.spinBox = QSpinBox(self)
        -        self.spinBox.setMaximumSize(QSize(150, 16777215))
        -        self.spinBox.setObjectName("spinBox")
        -        self.spinBox.setSuffix(" %")
        -        self.horizontalLayout_2.addWidget(self.spinBox)
        -        self.pushButton_2 = QPushButton(self)
        -        self.pushButton_2.setMaximumSize(QSize(150, 25))
        -        self.pushButton_2.setObjectName("pushButton_2")
        -        self.pushButton_2.setText("Set")
        -        self.horizontalLayout_2.addWidget(self.pushButton_2)
        -        self.verticalLayout.addLayout(self.horizontalLayout_2)
        -
        -

        Ancestors

        -
          -
        • PyQt5.QtWidgets.QGroupBox
        • -
        • PyQt5.QtWidgets.QWidget
        • -
        • PyQt5.QtCore.QObject
        • -
        • sip.wrapper
        • -
        • PyQt5.QtGui.QPaintDevice
        • -
        • sip.simplewrapper
        • -
        -
        -
        -class HeaterGroupbox -(parent, name) -
        -
        -

        QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

        -
        - -Expand source code - -
        class HeaterGroupbox(QGroupBox):
        -    def __init__(self, parent, name):
        -        super().__init__(parent=parent)
        -        self.setObjectName(name)
        -        self.resize(280, 407)
        -        self.setMaximumSize(QSize(280, 410))
        -        self.verticalLayout = QVBoxLayout(self)
        -        self.verticalLayout.setObjectName("verticalLayout")
        -        self.horizontalLayout = QHBoxLayout()
        -        self.horizontalLayout.setObjectName("horizontalLayout")
        -        self.label = QLabel(self)
        -        self.label.setObjectName("label")
        -        self.horizontalLayout.addWidget(self.label)
        -        spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        -        self.horizontalLayout.addItem(spacerItem)
        -        self.widget = LED(self)
        -        self.widget.setMinimumSize(QSize(25, 25))
        -        self.widget.setMaximumSize(QSize(25, 25))
        -        self.widget.setObjectName("widget")
        -        self.horizontalLayout.addWidget(self.widget)
        -        self.verticalLayout.addLayout(self.horizontalLayout)
        -        self.groupBox = ChannelGroupbox(self, "Channel A")
        -        self.groupBox.setTitle("")
        -        self.verticalLayout.addWidget(self.groupBox)
        -        self.groupBox_2 = ChannelGroupbox(self, "Channel B")
        -        self.groupBox_2.setTitle("")
        -        self.verticalLayout.addWidget(self.groupBox_2)
        -        self.groupBox_3 = ChannelGroupbox(self, "Channel C")
        -        self.groupBox_3.setTitle("")
        -        self.verticalLayout.addWidget(self.groupBox_3)
        -        self.groupBox_4 = ChannelGroupbox(self, "Channel D")
        -        self.groupBox_4.setTitle("")
        -        self.verticalLayout.addWidget(self.groupBox_4)   
        -
        -

        Ancestors

        -
          -
        • PyQt5.QtWidgets.QGroupBox
        • -
        • PyQt5.QtWidgets.QWidget
        • -
        • PyQt5.QtCore.QObject
        • -
        • sip.wrapper
        • -
        • PyQt5.QtGui.QPaintDevice
        • -
        • sip.simplewrapper
        • -
        -
        -
        -class HeaterMainWindow -(parent=None) -
        -
        -

        QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

        -
        - -Expand source code - -
        class HeaterMainWindow(QMainWindow, Observable):
        -    def __init__(self, parent=None):
        -        super().__init__(parent=parent)
        -        self.setWindowTitle("BeagleBone Black Heater Controller")
        -        
        -        self.setMaximumSize(QSize(1700, 420))
        -        
        -        self.horizontalLayout = QHBoxLayout()
        -        self.centralWidget = QWidget()
        -        self.centralWidget.setLayout(self.horizontalLayout)
        -        
        -        self.setCentralWidget(self.centralWidget)
        -        
        -        self.initUI()
        -        
        -        self.heater_monitoring_thread = QThread(self)
        -        self.heater_monitoring_worker = HeaterMonitoringWorker()
        -        self.heater_monitoring_worker.moveToThread(self.heater_monitoring_thread)
        -        
        -        self.pid_monitoring_thread = QThread(self)
        -        self.pid_monitoring_worker = PidMonitoringWorker()
        -        self.pid_monitoring_worker.moveToThread(self.pid_monitoring_thread)
        -
        -        self.heater_monitoring_worker.heater_enabled_status_signal.connect(self.on_heater_enabled_status_signal)
        -        self.heater_monitoring_worker.heater_duty_cycle_status_signal.connect(self.on_heater_duty_cycle_status_signal)
        -        
        -        self.pid_monitoring_worker.pid_enabled_status_signal.connect(self.on_pid_enabled_status_signal)
        -
        -        self.heater_monitoring_thread.started.connect(self.heater_monitoring_worker.start_process)
        -        self.heater_monitoring_thread.start()
        -        
        -        self.pid_monitoring_thread.started.connect(self.pid_monitoring_worker.start_process)
        -        self.pid_monitoring_thread.start()
        -        
        -    def initUI(self):
        -        setup = get_setup()
        -        
        -        self.htr_availability =  [True for available in setup.gse.beaglebone_heater.availability if available]
        -        self.heaters = [None] * len(self.htr_availability)
        -        self.channels = [None] * len(self.htr_availability)
        -        pid_configuration                = setup.gse.spid.configuration.heaters
        -        self.pids = [channel for heater in pid_configuration.values() for channel in heater]
        -        
        -        for idx, heater in enumerate(self.heaters):
        -            heater_name = "Heater {}".format(idx + 1)
        -            avail = self.htr_availability[idx]
        -            self.heaters[idx] = HeaterGroupbox(self, heater_name)
        -            self.heaters[idx].widget.set_color(1 if avail else 0)
        -            self.heaters[idx].label.setText(heater_name)
        -            self.horizontalLayout.addWidget(self.heaters[idx])
        -            
        -            self.channels[idx] = [None] * 4
        -            
        -            for ch_idx, channel in enumerate(self.channels[idx]):
        -                channel_name = "Channel A" if ch_idx == 0 else \
        -                                "Channel B" if ch_idx == 1 else \
        -                                "Channel C" if ch_idx == 2 else \
        -                                "Channel D"    
        -                self.channels[idx][ch_idx]  = self.heaters[idx].findChild(QGroupBox, channel_name)
        -                self.channels[idx][ch_idx].pushButton.hide()
        -                # channel.setObjectName(heater_name + channel_name)
        -                self.channels[idx][ch_idx].label.setText(channel_name)
        -                self.channels[idx][ch_idx].pushButton_2.clicked.connect(self.set_duty_cycle)
        -                self.channels[idx][ch_idx].checkBox.clicked.connect(self.set_enable)
        -                self.channels[idx][ch_idx].pushButton.clicked.connect(self.turn_off_pid)
        -        
        -    def on_heater_enabled_status_signal(self, monitoring_info: dict):
        -        for htr_idx, heater in enumerate(self.heaters):
        -            for ch_idx, channel in enumerate(self.channels[htr_idx]):
        -                channel.checkBox.set_selected(monitoring_info[htr_idx][ch_idx])
        -    
        -    def on_heater_duty_cycle_status_signal(self, monitoring_info: dict):
        -        for htr_idx, heater in enumerate(self.heaters):
        -            for ch_idx, channel in enumerate(self.channels[htr_idx]):
        -                channel.spinBox.setValue(int(monitoring_info[htr_idx][ch_idx] / 100))
        -    
        -    def on_pid_enabled_status_signal(self, monitoring_info: dict):
        -        for heater_channel, enabled in monitoring_info.items():
        -            command = heater_channel.split("_")
        -            htr = int(command[0])
        -            ch = int(command[1])
        -            if enabled:
        -
        -                self.channels[htr][ch].pushButton.show()
        -                self.channels[htr][ch].spinBox.setReadOnly(True)
        -                self.channels[htr][ch].setStyleSheet("""QGroupBox {border: 1px solid red;}""")
        -            
        -            else:
        -                self.channels[htr][ch].pushButton.hide()
        -                self.channels[htr][ch].spinBox.setReadOnly(False)
        -                self.channels[htr][ch].setStyleSheet('fusion') 
        -    
        -    def set_duty_cycle(self):
        -        htr = self.sender().parent().parent().objectName().split(" ")[1]
        -        channel = self.sender().parent().objectName().split(" ")[1]
        -        value = self.sender().parent().spinBox.value()
        -        duty_cycle = int(value * 100)
        -
        -        try:
        -            self.actionObservers({"set_duty_cycle": [int(htr)-1, int(ord(channel)-65), duty_cycle]})
        -        except Exception as e:
        -            warning_popup("set_duty_cycle : Heater {} Channel {} duty_cycle {}".format(
        -                htr, channel, duty_cycle
        -            ), e)
        -    
        -    
        -    def turn_off_pid(self):
        -        htr = self.sender().parent().parent().objectName().split(" ")[1]
        -        channel = self.sender().parent().objectName().split(" ")[1]
        -        
        -        for idx, pid in enumerate(self.pids):
        -            if (pid[3] is htr) and (pid[4] is channel):
        -                try:
        -                    self.actionObservers({"set_pid_disable": [int(idx)]}) if (pid[3] is htr and pid[4] is channel) else None
        -                except Exception as e:
        -                    warning_popup("set_pid_disable : {}".format(idx), e)
        -    
        -    def set_enable(self):
        -        htr = self.sender().parent().parent().objectName().split(" ")[1]
        -        channel = self.sender().parent().objectName().split(" ")[1]
        -        value = self.sender().is_selected()
        -        self.sender().set_selected(on=value)
        -        # logger.info("{} {} {}".format(int(htr), int(channel), bool(value)))
        -        try:
        -            self.actionObservers({"set_enable" : [int(htr)-1, int(ord(channel)-65), bool(value)]})
        -        except Exception as e:
        -            warning_popup('set_enable : Heater {} Channel {} to {}'.format(
        -                htr, channel, "Enabled" if value else "Disabled"
        -            ), e)
        -
        -

        Ancestors

        -
          -
        • PyQt5.QtWidgets.QMainWindow
        • -
        • PyQt5.QtWidgets.QWidget
        • -
        • PyQt5.QtCore.QObject
        • -
        • sip.wrapper
        • -
        • PyQt5.QtGui.QPaintDevice
        • -
        • sip.simplewrapper
        • -
        • Observable
        • -
        -

        Methods

        -
        -
        -def initUI(self) -
        -
        -
        -
        - -Expand source code - -
        def initUI(self):
        -    setup = get_setup()
        -    
        -    self.htr_availability =  [True for available in setup.gse.beaglebone_heater.availability if available]
        -    self.heaters = [None] * len(self.htr_availability)
        -    self.channels = [None] * len(self.htr_availability)
        -    pid_configuration                = setup.gse.spid.configuration.heaters
        -    self.pids = [channel for heater in pid_configuration.values() for channel in heater]
        -    
        -    for idx, heater in enumerate(self.heaters):
        -        heater_name = "Heater {}".format(idx + 1)
        -        avail = self.htr_availability[idx]
        -        self.heaters[idx] = HeaterGroupbox(self, heater_name)
        -        self.heaters[idx].widget.set_color(1 if avail else 0)
        -        self.heaters[idx].label.setText(heater_name)
        -        self.horizontalLayout.addWidget(self.heaters[idx])
        -        
        -        self.channels[idx] = [None] * 4
        -        
        -        for ch_idx, channel in enumerate(self.channels[idx]):
        -            channel_name = "Channel A" if ch_idx == 0 else \
        -                            "Channel B" if ch_idx == 1 else \
        -                            "Channel C" if ch_idx == 2 else \
        -                            "Channel D"    
        -            self.channels[idx][ch_idx]  = self.heaters[idx].findChild(QGroupBox, channel_name)
        -            self.channels[idx][ch_idx].pushButton.hide()
        -            # channel.setObjectName(heater_name + channel_name)
        -            self.channels[idx][ch_idx].label.setText(channel_name)
        -            self.channels[idx][ch_idx].pushButton_2.clicked.connect(self.set_duty_cycle)
        -            self.channels[idx][ch_idx].checkBox.clicked.connect(self.set_enable)
        -            self.channels[idx][ch_idx].pushButton.clicked.connect(self.turn_off_pid)
        -
        -
        -
        -def on_heater_duty_cycle_status_signal(self, monitoring_info: dict) -
        -
        -
        -
        - -Expand source code - -
        def on_heater_duty_cycle_status_signal(self, monitoring_info: dict):
        -    for htr_idx, heater in enumerate(self.heaters):
        -        for ch_idx, channel in enumerate(self.channels[htr_idx]):
        -            channel.spinBox.setValue(int(monitoring_info[htr_idx][ch_idx] / 100))
        -
        -
        -
        -def on_heater_enabled_status_signal(self, monitoring_info: dict) -
        -
        -
        -
        - -Expand source code - -
        def on_heater_enabled_status_signal(self, monitoring_info: dict):
        -    for htr_idx, heater in enumerate(self.heaters):
        -        for ch_idx, channel in enumerate(self.channels[htr_idx]):
        -            channel.checkBox.set_selected(monitoring_info[htr_idx][ch_idx])
        -
        -
        -
        -def on_pid_enabled_status_signal(self, monitoring_info: dict) -
        -
        -
        -
        - -Expand source code - -
        def on_pid_enabled_status_signal(self, monitoring_info: dict):
        -    for heater_channel, enabled in monitoring_info.items():
        -        command = heater_channel.split("_")
        -        htr = int(command[0])
        -        ch = int(command[1])
        -        if enabled:
        -
        -            self.channels[htr][ch].pushButton.show()
        -            self.channels[htr][ch].spinBox.setReadOnly(True)
        -            self.channels[htr][ch].setStyleSheet("""QGroupBox {border: 1px solid red;}""")
        -        
        -        else:
        -            self.channels[htr][ch].pushButton.hide()
        -            self.channels[htr][ch].spinBox.setReadOnly(False)
        -            self.channels[htr][ch].setStyleSheet('fusion') 
        -
        -
        -
        -def set_duty_cycle(self) -
        -
        -
        -
        - -Expand source code - -
        def set_duty_cycle(self):
        -    htr = self.sender().parent().parent().objectName().split(" ")[1]
        -    channel = self.sender().parent().objectName().split(" ")[1]
        -    value = self.sender().parent().spinBox.value()
        -    duty_cycle = int(value * 100)
        -
        -    try:
        -        self.actionObservers({"set_duty_cycle": [int(htr)-1, int(ord(channel)-65), duty_cycle]})
        -    except Exception as e:
        -        warning_popup("set_duty_cycle : Heater {} Channel {} duty_cycle {}".format(
        -            htr, channel, duty_cycle
        -        ), e)
        -
        -
        -
        -def set_enable(self) -
        -
        -
        -
        - -Expand source code - -
        def set_enable(self):
        -    htr = self.sender().parent().parent().objectName().split(" ")[1]
        -    channel = self.sender().parent().objectName().split(" ")[1]
        -    value = self.sender().is_selected()
        -    self.sender().set_selected(on=value)
        -    # logger.info("{} {} {}".format(int(htr), int(channel), bool(value)))
        -    try:
        -        self.actionObservers({"set_enable" : [int(htr)-1, int(ord(channel)-65), bool(value)]})
        -    except Exception as e:
        -        warning_popup('set_enable : Heater {} Channel {} to {}'.format(
        -            htr, channel, "Enabled" if value else "Disabled"
        -        ), e)
        -
        -
        -
        -def turn_off_pid(self) -
        -
        -
        -
        - -Expand source code - -
        def turn_off_pid(self):
        -    htr = self.sender().parent().parent().objectName().split(" ")[1]
        -    channel = self.sender().parent().objectName().split(" ")[1]
        -    
        -    for idx, pid in enumerate(self.pids):
        -        if (pid[3] is htr) and (pid[4] is channel):
        -            try:
        -                self.actionObservers({"set_pid_disable": [int(idx)]}) if (pid[3] is htr and pid[4] is channel) else None
        -            except Exception as e:
        -                warning_popup("set_pid_disable : {}".format(idx), e)
        -
        -
        -
        -
        -
        -class HeaterMonitoringWorker -
        -
        -

        Worker for monitoring the state and duty cycle of the Beaglebone Black Heaters

        -

        The worker will send a signal when a heater state changes or -when the duty cycle changes

        -

        Initialisation of a monitoring worker.

        -

        This worker keeps an eye on the monitoring port of the Beaglebone Heater. When a change in -Relevant information occurs, a signalw ill be emitted. These signals will be used to update the GUI

        -
        - -Expand source code - -
        class HeaterMonitoringWorker(QObject):
        -    """Worker for monitoring the state and duty cycle of the Beaglebone Black Heaters
        -
        -    The worker will send a signal when a heater state changes or 
        -        when the duty cycle changes
        -
        -    """
        -
        -    heater_enabled_status_signal        = pyqtSignal(list)
        -    heater_duty_cycle_status_signal                   = pyqtSignal(list)
        -    
        -    def __init__(self):
        -        """ Initialisation of a monitoring worker.
        -        
        -        This worker keeps an eye on the monitoring port of the Beaglebone Heater. When a change in
        -            Relevant information occurs, a signalw ill be emitted. These signals will be used to update the GUI
        -        """
        -        
        -        
        -        super(HeaterMonitoringWorker, self).__init__()
        -        
        -        self.active = False
        -        self.just_reconnected = True
        -        
        -        self.monitoring_socket = None
        -        self.is_socket_connected = True
        -        self.monitoring_timeout = 0.5
        -        
        -        self.connect_socket()
        -        
        -        # Keep track of the heater status, so we only have to send a signal when the state has changed
        -        
        -        self.previous_heater_status_signal = {}
        -        self.previous_heater_duty_cycle    = {}
        -        
        -    def connect_socket(self):
        -        """ Create a socket and connect to the monitoring port.
        -        """
        -        
        -
        -        try:
        -            transport   = HEATER_CTRL_SETTINGS.PROTOCOL
        -            hostname    = HEATER_CTRL_SETTINGS.HOSTNAME
        -            
        -            monitoring_port = HEATER_CTRL_SETTINGS.MONITORING_PORT
        -            monitoring_address = connect_address(transport, hostname, monitoring_port)
        -            
        -            self.monitoring_socket = zmq.Context().socket(zmq.SUB)
        -            self.monitoring_socket.connect(monitoring_address)
        -            self.monitoring_socket.setsockopt_string(zmq.SUBSCRIBE, "")
        -            
        -            self.monitoring_timeout = 0.5
        -            
        -            self.is_socket_connected = True
        -            
        -        except:
        -            self.is_socket_connected = False
        -            
        -    def stop(self):
        -        
        -        """ Stop the monitoring worker.
        -
        -        The monitoring socket is disconnected from the monitoring port and is then closed immediately.
        -        """
        -        
        -        self.monitoring_socket.close()
        -        self.is_socket_connected = False
        -        
        -        self.active = False
        -        
        -    def start_process(self):
        -        """Start updated the Beaglebone status"""
        -        self.run()
        -        
        -    @pyqtSlot()
        -    def run(self):
        -        """Keep on checkin whether the Beaglebone state has changed
        -        
        -        If the beaglebone status has changed, update it in the GUI
        -
        -        Raises:
        -            Exception: ZMQ Error
        -        """
        -        
        -        self.active = True
        -        while self.is_socket_connected and self.active:
        -            
        -            try:
        -                socket_list, _, exc_list = zmq.select([self.monitoring_socket], [], [], timeout=self.monitoring_timeout)
        -
        -                if self.monitoring_socket in socket_list:
        -                    try:
        -                        pickle_string = self.monitoring_socket.recv()
        -                    except Exception:
        -                        raise Exception
        -
        -                    monitoring_info = pickle.loads(pickle_string)
        -                    heater_enabled      = monitoring_info['Enabled']
        -                    heater_duty_cycle   = monitoring_info['duty_cycle']
        -
        -                    self.heater_enabled_status_signal.emit(heater_enabled)
        -                    
        -                    if heater_duty_cycle != self.previous_heater_duty_cycle:
        -                        self.heater_duty_cycle_status_signal.emit(heater_duty_cycle)
        -                    
        -                    self.previous_heater_duty_cycle = heater_duty_cycle
        -                    
        -                    
        -            except ZMQError as exc:
        -                raise exc
        -
        -

        Ancestors

        -
          -
        • PyQt5.QtCore.QObject
        • -
        • sip.wrapper
        • -
        • sip.simplewrapper
        • -
        -

        Methods

        -
        -
        -def connect_socket(self) -
        -
        -

        Create a socket and connect to the monitoring port.

        -
        - -Expand source code - -
        def connect_socket(self):
        -    """ Create a socket and connect to the monitoring port.
        -    """
        -    
        -
        -    try:
        -        transport   = HEATER_CTRL_SETTINGS.PROTOCOL
        -        hostname    = HEATER_CTRL_SETTINGS.HOSTNAME
        -        
        -        monitoring_port = HEATER_CTRL_SETTINGS.MONITORING_PORT
        -        monitoring_address = connect_address(transport, hostname, monitoring_port)
        -        
        -        self.monitoring_socket = zmq.Context().socket(zmq.SUB)
        -        self.monitoring_socket.connect(monitoring_address)
        -        self.monitoring_socket.setsockopt_string(zmq.SUBSCRIBE, "")
        -        
        -        self.monitoring_timeout = 0.5
        -        
        -        self.is_socket_connected = True
        -        
        -    except:
        -        self.is_socket_connected = False
        -
        -
        -
        -def heater_duty_cycle_status_signal(...) -
        -
        -
        -
        -
        -def heater_enabled_status_signal(...) -
        -
        -
        -
        -
        -def run(self) -
        -
        -

        Keep on checkin whether the Beaglebone state has changed

        -

        If the beaglebone status has changed, update it in the GUI

        -

        Raises

        -
        -
        Exception
        -
        ZMQ Error
        -
        -
        - -Expand source code - -
        @pyqtSlot()
        -def run(self):
        -    """Keep on checkin whether the Beaglebone state has changed
        -    
        -    If the beaglebone status has changed, update it in the GUI
        -
        -    Raises:
        -        Exception: ZMQ Error
        -    """
        -    
        -    self.active = True
        -    while self.is_socket_connected and self.active:
        -        
        -        try:
        -            socket_list, _, exc_list = zmq.select([self.monitoring_socket], [], [], timeout=self.monitoring_timeout)
        -
        -            if self.monitoring_socket in socket_list:
        -                try:
        -                    pickle_string = self.monitoring_socket.recv()
        -                except Exception:
        -                    raise Exception
        -
        -                monitoring_info = pickle.loads(pickle_string)
        -                heater_enabled      = monitoring_info['Enabled']
        -                heater_duty_cycle   = monitoring_info['duty_cycle']
        -
        -                self.heater_enabled_status_signal.emit(heater_enabled)
        -                
        -                if heater_duty_cycle != self.previous_heater_duty_cycle:
        -                    self.heater_duty_cycle_status_signal.emit(heater_duty_cycle)
        -                
        -                self.previous_heater_duty_cycle = heater_duty_cycle
        -                
        -                
        -        except ZMQError as exc:
        -            raise exc
        -
        -
        -
        -def start_process(self) -
        -
        -

        Start updated the Beaglebone status

        -
        - -Expand source code - -
        def start_process(self):
        -    """Start updated the Beaglebone status"""
        -    self.run()
        -
        -
        -
        -def stop(self) -
        -
        -

        Stop the monitoring worker.

        -

        The monitoring socket is disconnected from the monitoring port and is then closed immediately.

        -
        - -Expand source code - -
        def stop(self):
        -    
        -    """ Stop the monitoring worker.
        -
        -    The monitoring socket is disconnected from the monitoring port and is then closed immediately.
        -    """
        -    
        -    self.monitoring_socket.close()
        -    self.is_socket_connected = False
        -    
        -    self.active = False
        -
        -
        -
        -
        -
        -class HeaterUIController -(model: HeaterUIModel, view: HeaterMainWindow) -
        -
        -

        Helper class that provides a standard way to create an ABC using -inheritance.

        -
        - -Expand source code - -
        class HeaterUIController(Observer):
        -    def __init__(self, model: HeaterUIModel, view: HeaterMainWindow):
        -        self.model  = model
        -        self.view   = view
        -        self.view.addObserver(self)
        - 
        -    def update(self, changed_object):
        -
        -        text = changed_object.text()
        -
        -        if text == "Reconnect":
        -
        -            if changed_object.isChecked():
        -
        -                logger.debug("Reconnecting the Heater model.")
        -
        -                if self.model.reconnect():
        -
        -                    self.view.set_connection_state(True)
        -
        -                    if not self.model.has_commands():
        -
        -                        self.model.load_commands()
        -
        -                else:
        -                    self.view.reconnect_action.setChecked(False)
        -            else:
        -
        -                logger.debug("Disconnecting the Heater model.")
        -                self.model.disconnect()
        -                self.view.set_connection_state(False)
        -
        -            return
        - 
        -    
        -    def do(self, actions):
        -        for action, value in actions.items():
        -            logger.debug(f"do {action} with {value}")
        -            if action == "set_duty_cycle":
        -                htr_idx     = value[0]
        -                ch_idx      = value[1]
        -                duty_cycle  = value[2]
        -                logger.debug(f"do: set_duty_cycle({htr_idx}, {ch_idx}, {duty_cycle})")
        -                self.model.set_duty_cycle(htr_idx, ch_idx, duty_cycle)
        -
        -                
        -            if action == "set_enable":
        -                htr_idx     = value[0]
        -                ch_idx      = value[1]
        -                state  = value[2]
        -                logger.debug(f"do: set_enable({htr_idx}, {ch_idx}, {state})")
        -                self.model.set_enable(htr_idx, ch_idx, state)
        -                
        -            if action == "set_pid_enable":
        -                ch          = value[0]
        -                logger.debug(f"do: set_pid_enable({ch}")
        -                self.model.set_pid_enable(ch)
        -                
        -            if action == "set_pid_disable":
        -                ch          = value[0]
        -                logger.info(f"do: set_pid_disable({ch})")
        -                self.model.set_pid_disable(ch)
        -
        -

        Ancestors

        - -

        Methods

        -
        -
        -def do(self, actions) -
        -
        -
        -
        - -Expand source code - -
        def do(self, actions):
        -    for action, value in actions.items():
        -        logger.debug(f"do {action} with {value}")
        -        if action == "set_duty_cycle":
        -            htr_idx     = value[0]
        -            ch_idx      = value[1]
        -            duty_cycle  = value[2]
        -            logger.debug(f"do: set_duty_cycle({htr_idx}, {ch_idx}, {duty_cycle})")
        -            self.model.set_duty_cycle(htr_idx, ch_idx, duty_cycle)
        -
        -            
        -        if action == "set_enable":
        -            htr_idx     = value[0]
        -            ch_idx      = value[1]
        -            state  = value[2]
        -            logger.debug(f"do: set_enable({htr_idx}, {ch_idx}, {state})")
        -            self.model.set_enable(htr_idx, ch_idx, state)
        -            
        -        if action == "set_pid_enable":
        -            ch          = value[0]
        -            logger.debug(f"do: set_pid_enable({ch}")
        -            self.model.set_pid_enable(ch)
        -            
        -        if action == "set_pid_disable":
        -            ch          = value[0]
        -            logger.info(f"do: set_pid_disable({ch})")
        -            self.model.set_pid_disable(ch)
        -
        -
        -
        -def update(self, changed_object) -
        -
        -
        -
        - -Expand source code - -
        def update(self, changed_object):
        -
        -    text = changed_object.text()
        -
        -    if text == "Reconnect":
        -
        -        if changed_object.isChecked():
        -
        -            logger.debug("Reconnecting the Heater model.")
        -
        -            if self.model.reconnect():
        -
        -                self.view.set_connection_state(True)
        -
        -                if not self.model.has_commands():
        -
        -                    self.model.load_commands()
        -
        -            else:
        -                self.view.reconnect_action.setChecked(False)
        -        else:
        -
        -            logger.debug("Disconnecting the Heater model.")
        -            self.model.disconnect()
        -            self.view.set_connection_state(False)
        -
        -        return
        -
        -
        -
        -
        -
        -class HeaterUIModel -(mode) -
        -
        -
        -
        - -Expand source code - -
        class HeaterUIModel:
        -    def __init__(self, mode):
        -            self.mode = mode
        -
        -            if self.mode == 'proxy':
        -                try:
        -                    self.heater = BeagleboneProxy()
        -                except Exception as exc:
        -                    raise exc
        -
        -            elif self.mode == 'simulator':
        -                self.heater = BeagleboneSimulator()
        -            else:
        -                raise ValueError(f'Unknown type of Heater implementation passed into the model')
        -            
        -            if self.heater is not None:
        -                logger.debug(f'Heater Controller initialized as {self.heater.__class__.__name__}')
        -
        -
        -    def set_enable(self, htr_idx, ch_idx, enable):
        -        with BeagleboneProxy() as heater:
        -            heater.set_enable(htr_idx, ch_idx, enable)
        -        
        -    def set_duty_cycle(self, htr_idx, ch_idx, duty_cycle):
        -        with BeagleboneProxy() as heater:
        -            heater.set_duty_cycle(htr_idx, ch_idx, duty_cycle)
        -
        -    def set_pid_enable(self, ch):
        -        try:
        -            with PidProxy() as pid:
        -                pid.enable(ch)
        -        except:
        -            logger.info("PID Control Server could not be found")
        -        
        -    def set_pid_disable(self, ch):
        -        logger.info(f"PID channel {ch} has been disabled")
        -        try:
        -            with PidProxy() as pid:
        -                pid.disable(ch)
        -        except:
        -            logger.info("PID Control Server could not be found")
        -    
        -    def has_commands(self):
        -        if self.mode == "proxy":
        -            with BeagleboneProxy() as heater:
        -                return heater.has_commands()
        -
        -        return True
        -
        -    def load_commands(self):
        -        if self.mode == "proxy":
        -            with BeagleboneProxy() as heater:
        -                with PidProxy() as pid:
        -                    pid.load_commands()
        -                    heater.load_commands()
        -
        -    def is_simulator(self):
        -        with BeagleboneProxy() as heater:
        -            return heater.is_simulator()
        -
        -    def is_connected(self):
        -        if self.mode == "proxy":
        -            with BeagleboneProxy() as heater:
        -                return heater.is_cs_connected()
        -
        -        else:
        -
        -            return self.heater.is_connected()
        -
        -    def reconnect(self):
        -        if self.mode == "proxy":
        -            with BeagleboneProxy() as heater:
        -
        -                heater.reconnect_cs()
        -
        -                return heater.is_cs_connected()
        -
        -        else:
        -
        -            self.heater.reconnect()
        -
        -            return self.heater.is_connected()
        -
        -        return False
        -
        -    def disconnect(self):
        -        if self.mode == "proxy":
        -
        -            self.mode.disconnect_cs()
        -
        -

        Methods

        -
        -
        -def disconnect(self) -
        -
        -
        -
        - -Expand source code - -
        def disconnect(self):
        -    if self.mode == "proxy":
        -
        -        self.mode.disconnect_cs()
        -
        -
        -
        -def has_commands(self) -
        -
        -
        -
        - -Expand source code - -
        def has_commands(self):
        -    if self.mode == "proxy":
        -        with BeagleboneProxy() as heater:
        -            return heater.has_commands()
        -
        -    return True
        -
        -
        -
        -def is_connected(self) -
        -
        -
        -
        - -Expand source code - -
        def is_connected(self):
        -    if self.mode == "proxy":
        -        with BeagleboneProxy() as heater:
        -            return heater.is_cs_connected()
        -
        -    else:
        -
        -        return self.heater.is_connected()
        -
        -
        -
        -def is_simulator(self) -
        -
        -
        -
        - -Expand source code - -
        def is_simulator(self):
        -    with BeagleboneProxy() as heater:
        -        return heater.is_simulator()
        -
        -
        -
        -def load_commands(self) -
        -
        -
        -
        - -Expand source code - -
        def load_commands(self):
        -    if self.mode == "proxy":
        -        with BeagleboneProxy() as heater:
        -            with PidProxy() as pid:
        -                pid.load_commands()
        -                heater.load_commands()
        -
        -
        -
        -def reconnect(self) -
        -
        -
        -
        - -Expand source code - -
        def reconnect(self):
        -    if self.mode == "proxy":
        -        with BeagleboneProxy() as heater:
        -
        -            heater.reconnect_cs()
        -
        -            return heater.is_cs_connected()
        -
        -    else:
        -
        -        self.heater.reconnect()
        -
        -        return self.heater.is_connected()
        -
        -    return False
        -
        -
        -
        -def set_duty_cycle(self, htr_idx, ch_idx, duty_cycle) -
        -
        -
        -
        - -Expand source code - -
        def set_duty_cycle(self, htr_idx, ch_idx, duty_cycle):
        -    with BeagleboneProxy() as heater:
        -        heater.set_duty_cycle(htr_idx, ch_idx, duty_cycle)
        -
        -
        -
        -def set_enable(self, htr_idx, ch_idx, enable) -
        -
        -
        -
        - -Expand source code - -
        def set_enable(self, htr_idx, ch_idx, enable):
        -    with BeagleboneProxy() as heater:
        -        heater.set_enable(htr_idx, ch_idx, enable)
        -
        -
        -
        -def set_pid_disable(self, ch) -
        -
        -
        -
        - -Expand source code - -
        def set_pid_disable(self, ch):
        -    logger.info(f"PID channel {ch} has been disabled")
        -    try:
        -        with PidProxy() as pid:
        -            pid.disable(ch)
        -    except:
        -        logger.info("PID Control Server could not be found")
        -
        -
        -
        -def set_pid_enable(self, ch) -
        -
        -
        -
        - -Expand source code - -
        def set_pid_enable(self, ch):
        -    try:
        -        with PidProxy() as pid:
        -            pid.enable(ch)
        -    except:
        -        logger.info("PID Control Server could not be found")
        -
        -
        -
        -
        -
        -class PidMonitoringWorker -
        -
        -

        Worker for monitoring the state of the individual PID channels

        -

        The worker will send a signal when the state of a PID channel has changed

        -
        - -Expand source code - -
        class PidMonitoringWorker(QObject):
        -    """Worker for monitoring the state of the individual PID channels
        -
        -   The worker will send a signal when the state of a PID channel has changed
        -
        -    """
        -    
        -    pid_control_server_status_signal = pyqtSignal(bool)
        -    pid_enabled_status_signal        = pyqtSignal(dict)
        -    
        -    def __init__(self):
        -        super(PidMonitoringWorker, self).__init__()
        -        
        -        self.active = False
        -        self.just_reconnected = True
        -        
        -        self.enabled = False
        -        self.pid_enabled = []
        -        
        -        self.monitoring_socket = None
        -        self.is_socket_connected = True
        -        self.monitoring_timeout = 0.5
        -        
        -        self.connect_socket()
        -        
        -        self.previous_control_server_status_signal = ()
        -        self.previous_pid_enabled_status_signal = {}
        -
        -
        -    def connect_socket(self):
        -        try:
        -            transport   = PID_CTRL_SETTINGS.PROTOCOL
        -            hostname    = PID_CTRL_SETTINGS.HOSTNAME
        -            
        -            monitoring_port = PID_CTRL_SETTINGS.MONITORING_PORT
        -            monitoring_address = connect_address(transport, hostname, monitoring_port)
        -            self.monitoring_socket = zmq.Context().socket(zmq.SUB)
        -            self.monitoring_socket.connect(monitoring_address)
        -            self.monitoring_socket.setsockopt_string(zmq.SUBSCRIBE, "")
        -            
        -            
        -            self.monitoring_timeout = 0.5
        -            
        -            self.is_socket_connected = True
        -            
        -        except:
        -            self.is_socket_connected = False
        -            
        -    def stop(self):
        -        self.monitoring_socket.close()
        -        self.is_socket_connected = False
        -        
        -        self.active = False
        -        
        -    def start_process(self):
        -        self.run()
        -        
        -    @pyqtSlot()
        -    def run(self):
        -        self.active = True
        -        while self.is_socket_connected and self.active:
        -            
        -            try:
        -                socket_list, _, exc_list = zmq.select([self.monitoring_socket], [], [], timeout=self.monitoring_timeout)
        -                if self.monitoring_socket in socket_list:
        -                    self.enabled = True
        -                    pickle_string = self.monitoring_socket.recv()
        -
        -                    monitoring_info = pickle.loads(pickle_string)
        -                    self.pid_enabled      = monitoring_info['Enabled']
        -                    
        -                    if self.previous_pid_enabled_status_signal != self.pid_enabled:
        -                        self.pid_enabled_status_signal.emit(self.pid_enabled)
        -                    
        -                if self.previous_control_server_status_signal != self.enabled:
        -                    self.pid_control_server_status_signal.emit(self.enabled)
        -                    
        -                self.previous_control_server_status_signal = self.enabled
        -                self.previous_pid_enabled_status_signal = self.pid_enabled
        -            except ZMQError as exc:
        -                raise exc
        -
        -

        Ancestors

        -
          -
        • PyQt5.QtCore.QObject
        • -
        • sip.wrapper
        • -
        • sip.simplewrapper
        • -
        -

        Methods

        -
        -
        -def connect_socket(self) -
        -
        -
        -
        - -Expand source code - -
        def connect_socket(self):
        -    try:
        -        transport   = PID_CTRL_SETTINGS.PROTOCOL
        -        hostname    = PID_CTRL_SETTINGS.HOSTNAME
        -        
        -        monitoring_port = PID_CTRL_SETTINGS.MONITORING_PORT
        -        monitoring_address = connect_address(transport, hostname, monitoring_port)
        -        self.monitoring_socket = zmq.Context().socket(zmq.SUB)
        -        self.monitoring_socket.connect(monitoring_address)
        -        self.monitoring_socket.setsockopt_string(zmq.SUBSCRIBE, "")
        -        
        -        
        -        self.monitoring_timeout = 0.5
        -        
        -        self.is_socket_connected = True
        -        
        -    except:
        -        self.is_socket_connected = False
        -
        -
        -
        -def pid_control_server_status_signal(...) -
        -
        -
        -
        -
        -def pid_enabled_status_signal(...) -
        -
        -
        -
        -
        -def run(self) -
        -
        -
        -
        - -Expand source code - -
        @pyqtSlot()
        -def run(self):
        -    self.active = True
        -    while self.is_socket_connected and self.active:
        -        
        -        try:
        -            socket_list, _, exc_list = zmq.select([self.monitoring_socket], [], [], timeout=self.monitoring_timeout)
        -            if self.monitoring_socket in socket_list:
        -                self.enabled = True
        -                pickle_string = self.monitoring_socket.recv()
        -
        -                monitoring_info = pickle.loads(pickle_string)
        -                self.pid_enabled      = monitoring_info['Enabled']
        -                
        -                if self.previous_pid_enabled_status_signal != self.pid_enabled:
        -                    self.pid_enabled_status_signal.emit(self.pid_enabled)
        -                
        -            if self.previous_control_server_status_signal != self.enabled:
        -                self.pid_control_server_status_signal.emit(self.enabled)
        -                
        -            self.previous_control_server_status_signal = self.enabled
        -            self.previous_pid_enabled_status_signal = self.pid_enabled
        -        except ZMQError as exc:
        -            raise exc
        -
        -
        -
        -def start_process(self) -
        -
        -
        -
        - -Expand source code - -
        def start_process(self):
        -    self.run()
        -
        -
        -
        -def stop(self) -
        -
        -
        -
        - -Expand source code - -
        def stop(self):
        -    self.monitoring_socket.close()
        -    self.is_socket_connected = False
        -    
        -    self.active = False
        -
        -
        -
        -
        -
        -
        -
        - -
        - - - \ No newline at end of file diff --git a/docs/api/egse/tempcontrol/spid/spid.html b/docs/api/egse/tempcontrol/digalox/digalox.html similarity index 50% rename from docs/api/egse/tempcontrol/spid/spid.html rename to docs/api/egse/tempcontrol/digalox/digalox.html index 3961fa6..6e7345a 100644 --- a/docs/api/egse/tempcontrol/spid/spid.html +++ b/docs/api/egse/tempcontrol/digalox/digalox.html @@ -4,7 +4,7 @@ -egse.tempcontrol.spid.spid API documentation +egse.tempcontrol.digalox.digalox API documentation @@ -19,7 +19,7 @@
        -

        Module egse.tempcontrol.spid.spid

        +

        Module egse.tempcontrol.digalox.digalox

        @@ -27,114 +27,120 @@

        Module egse.tempcontrol.spid.spid

        Expand source code
        import logging
        -from random import seed, randint
        +import random
        +
         from egse.command import ClientServerCommand
        +from egse.proxy import Proxy
         from egse.decorators import dynamic_interface
         from egse.device import DeviceInterface
        -from egse.proxy import Proxy
        +from egse.serialdevice import SerialDevice
         from egse.settings import Settings
         from egse.zmq_ser import connect_address
         
         logger = logging.getLogger(__name__)
         
        -CTRL_SETTINGS = Settings.load("SPID Control Server")
        -DEVICE_SETTINGS = Settings.load(filename='spid.yaml')
        +DEVICE_SETTINGS = Settings.load(filename='digalox.yaml')
        +CTRL_SETTINGS = Settings.load("Digalox LN2 level monitor Control Server")
         
         
        -class PidError(Exception):
        +class DigaloxError(Exception):
             pass
         
         
        -class PidCommand(ClientServerCommand):
        +class DigaloxCommand(ClientServerCommand):
             def get_cmd_string(self, *args, **kwargs):
        -        return super().get_cmd_string(*args, **kwargs)
        -
        -
        -class PidInterface(DeviceInterface):
        -    """ PID base class."""
        +        out = super().get_cmd_string(*args, **kwargs)
        +        return out + '\n'
         
        -    @dynamic_interface
        -    def set_temperature(self, heater_index, setpoint_temperature):
        -        """ PID setpoint temperature. """
        -        return NotImplemented
        -
        -    @dynamic_interface
        -    def get_temperature(self, heater_index):
        -        return NotImplemented
         
        -    @dynamic_interface
        -    def enable(self, channel):
        -        return NotImplemented
        +class DigaloxInterface(DeviceInterface):
        +    """ Digalox LN2 level monitor Interface base class."""
         
             @dynamic_interface
        -    def disable(self, channel):
        -        return NotImplemented
        -    
        -    @dynamic_interface
        -    def get_running(self, channel):
        -        return NotImplemented
        -    
        -    @dynamic_interface
        -    def set_pid_parameters(self, channel, Kp, Ki, Kd, int_max=1000, int_min=0, reset=True):
        -        return NotImplemented
        -    
        -    @dynamic_interface
        -    def get_pid_parameters(self, channel):
        +    def get_value(self):
        +        """ Get Digalox value """
                 return NotImplemented
         
         
        -class PidSimulator(PidInterface):
        -   """ PID simulator class. """
        -
        -   def __init__(self):
        -       self._is_connected = True
        -       seed()
        -       self.running = [1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0]
        -       self.setpoints = [randint(-180, +250) for x in range(0, 14)]
        -       self.temperature = [randint(-180, +250) for x in range(0, 14)]
        -       self.timestamp =  [0 for x in range(0, 14)]
        -       self.errors   = [randint(0, 200) for x in range(0, 14)]
        -       self.isum = [randint(0, 900) for x in range(0, 14)]
        -       self.inputs = [randint(-180, +250) for x in range(0, 14)]
        -       self.outputs = [randint(-180, +250) for x in range(0, 14)]
        -
        -
        +class DigaloxSimulator(DigaloxInterface):
        +    def __init__(self):
        +        self._is_connected = True
         
        -   def is_connected(self):
        +    def is_connected(self):
                return self._is_connected
         
        -   def is_simulator(self):
        +    def is_simulator(self):
                return True
         
        -   def connect(self):
        +    def connect(self):
                self._is_connected = True
         
        -   def disconnect(self):
        +    def disconnect(self):
                self._is_connected = False
         
        -   def reconnect(self):
        +    def reconnect(self):
                if self.is_connected():
                    self.disconnect()
                self.connect()
         
        -   def set_temperature(self, heater_idx, setpoint_temperature):
        -       logger.info("Settings temperature forPID {heater_idx} to {setpoint}")
        -       self.setpoints[heater_idx] = setpoint_temperature
        -       self.temperature[heater_idx] = setpoint_temperature - 10
        -
        -   def enable(self, channel):
        -       logger.info(f"Enabling {channel}")
        -       self.running[channel] = 1
        -
        -   def disable(self, channel):
        -       logger.info(f"Disabling {channel}")
        -       self.running[channel] = 0
        -
        -class PidProxy(Proxy, PidInterface):
        -    def __init__(self, protocol=CTRL_SETTINGS.PROTOCOL,
        -                 hostname=CTRL_SETTINGS.HOSTNAME,
        -                 port=CTRL_SETTINGS.COMMANDING_PORT):
        -        super().__init__(connect_address(protocol, hostname, port))
        + def get_value(self): + return random.random() + + +class DigaloxController(DigaloxInterface): + + def __init__(self): + super().__init__() + + logger.debug('Initializing Digalox controller') + + settings = Settings.load(f'Digalox LN2 level monitor Controller') + self._port = settings.PORT + self._baudrate = settings.BAUDRATE + + try: + self.digalox = SerialDevice(self._port , + baudrate=self._baudrate, + terminator='\r', + timeout=2) + except ConnectionError as ex: + raise DigaloxError("Could not connect to the Digalox LN2 level monitor") from ex + self.digalox.connect() + + def is_simulator(self): + return False + + def is_connected(self): + if self.digalox._serial is None: + return False + + if not self.digalox._serial.is_open: + return False + + return True + + def connect(self): + if not self.digalox.is_connected(): + self.digalox.connect() + + def disconnect(self): + self.digalox.disconnect() + + def reconnect(self): + self.digalox.reconnect() + + def get_value(self): + response = self.digalox.query('value?\r') + value = response.split(';')[-1] + return float(value) + + +class DigaloxProxy(Proxy, DigaloxInterface): + def __init__(self): + super().__init__(connect_address(CTRL_SETTINGS.PROTOCOL, + CTRL_SETTINGS.HOSTNAME, + CTRL_SETTINGS.COMMANDING_PORT), + timeout=20000)

  • @@ -146,8 +152,8 @@

    Module egse.tempcontrol.spid.spid

    Classes

    -
    -class PidCommand +
    +class DigaloxCommand (name, cmd, response=None, wait=None, check=None, description=None, device_method=None)
    @@ -161,9 +167,10 @@

    Classes

    Expand source code -
    class PidCommand(ClientServerCommand):
    +
    class DigaloxCommand(ClientServerCommand):
         def get_cmd_string(self, *args, **kwargs):
    -        return super().get_cmd_string(*args, **kwargs)
    + out = super().get_cmd_string(*args, **kwargs) + return out + '\n'

    Ancestors

      @@ -172,7 +179,7 @@

      Ancestors

    Methods

    -
    +
    def get_cmd_string(self, *args, **kwargs)
    @@ -182,7 +189,8 @@

    Methods

    Expand source code
    def get_cmd_string(self, *args, **kwargs):
    -    return super().get_cmd_string(*args, **kwargs)
    + out = super().get_cmd_string(*args, **kwargs) + return out + '\n'
    @@ -196,8 +204,87 @@

    Inherited members

    -
    -class PidError +
    +class DigaloxController +
    +
    +

    Digalox LN2 level monitor Interface base class.

    +
    + +Expand source code + +
    class DigaloxController(DigaloxInterface):
    +
    +    def __init__(self):
    +        super().__init__()
    +
    +        logger.debug('Initializing Digalox controller')
    +        
    +        settings = Settings.load(f'Digalox LN2 level monitor Controller')
    +        self._port = settings.PORT
    +        self._baudrate = settings.BAUDRATE
    +        
    +        try:
    +            self.digalox = SerialDevice(self._port ,
    +                                        baudrate=self._baudrate,
    +                                        terminator='\r',
    +                                        timeout=2)
    +        except ConnectionError as ex:
    +            raise DigaloxError("Could not connect to the Digalox LN2 level monitor") from ex
    +        self.digalox.connect()
    +        
    +    def is_simulator(self):
    +        return False
    +
    +    def is_connected(self):
    +        if self.digalox._serial is None:
    +            return False
    +
    +        if not self.digalox._serial.is_open:
    +            return False
    +
    +        return True
    +
    +    def connect(self):
    +        if not self.digalox.is_connected():
    +            self.digalox.connect()
    +
    +    def disconnect(self):
    +        self.digalox.disconnect()
    +
    +    def reconnect(self):
    +        self.digalox.reconnect()
    +
    +    def get_value(self):
    +        response = self.digalox.query('value?\r')
    +        value = response.split(';')[-1]
    +        return float(value)
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    +
    +class DigaloxError (*args, **kwargs)
    @@ -206,7 +293,7 @@

    Inherited members

    Expand source code -
    class PidError(Exception):
    +
    class DigaloxError(Exception):
         pass

    Ancestors

    @@ -215,45 +302,21 @@

    Ancestors

  • builtins.BaseException
  • -
    -class PidInterface +
    +class DigaloxInterface
    -

    PID base class.

    +

    Digalox LN2 level monitor Interface base class.

    Expand source code -
    class PidInterface(DeviceInterface):
    -    """ PID base class."""
    -
    -    @dynamic_interface
    -    def set_temperature(self, heater_index, setpoint_temperature):
    -        """ PID setpoint temperature. """
    -        return NotImplemented
    -
    -    @dynamic_interface
    -    def get_temperature(self, heater_index):
    -        return NotImplemented
    +
    class DigaloxInterface(DeviceInterface):
    +    """ Digalox LN2 level monitor Interface base class."""
     
         @dynamic_interface
    -    def enable(self, channel):
    -        return NotImplemented
    -
    -    @dynamic_interface
    -    def disable(self, channel):
    -        return NotImplemented
    -    
    -    @dynamic_interface
    -    def get_running(self, channel):
    -        return NotImplemented
    -    
    -    @dynamic_interface
    -    def set_pid_parameters(self, channel, Kp, Ki, Kd, int_max=1000, int_min=0, reset=True):
    -        return NotImplemented
    -    
    -    @dynamic_interface
    -    def get_pid_parameters(self, channel):
    +    def get_value(self):
    +        """ Get Digalox value """
             return NotImplemented

    Ancestors

    @@ -264,108 +327,24 @@

    Ancestors

    Subclasses

    Methods

    -
    -def disable(self, channel) -
    -
    -
    -
    - -Expand source code - -
    @dynamic_interface
    -def disable(self, channel):
    -    return NotImplemented
    -
    -
    -
    -def enable(self, channel) -
    -
    -
    -
    - -Expand source code - -
    @dynamic_interface
    -def enable(self, channel):
    -    return NotImplemented
    -
    -
    -
    -def get_pid_parameters(self, channel) -
    -
    -
    -
    - -Expand source code - -
    @dynamic_interface
    -def get_pid_parameters(self, channel):
    -    return NotImplemented
    -
    -
    -
    -def get_running(self, channel) -
    -
    -
    -
    - -Expand source code - -
    @dynamic_interface
    -def get_running(self, channel):
    -    return NotImplemented
    -
    -
    -
    -def get_temperature(self, heater_index) -
    -
    -
    -
    - -Expand source code - -
    @dynamic_interface
    -def get_temperature(self, heater_index):
    -    return NotImplemented
    -
    -
    -
    -def set_pid_parameters(self, channel, Kp, Ki, Kd, int_max=1000, int_min=0, reset=True) +
    +def get_value(self)
    -
    +

    Get Digalox value

    Expand source code
    @dynamic_interface
    -def set_pid_parameters(self, channel, Kp, Ki, Kd, int_max=1000, int_min=0, reset=True):
    -    return NotImplemented
    -
    -
    -
    -def set_temperature(self, heater_index, setpoint_temperature) -
    -
    -

    PID setpoint temperature.

    -
    - -Expand source code - -
    @dynamic_interface
    -def set_temperature(self, heater_index, setpoint_temperature):
    -    """ PID setpoint temperature. """
    +def get_value(self):
    +    """ Get Digalox value """
         return NotImplemented
    @@ -385,9 +364,8 @@

    Inherited members

    -
    -class PidProxy -(protocol='tcp', hostname='localhost', port=6935) +
    +class DigaloxProxy

    A Proxy object will forward CommandExecutions to the connected control server @@ -404,18 +382,19 @@

    Inherited members

    Expand source code -
    class PidProxy(Proxy, PidInterface):
    -    def __init__(self, protocol=CTRL_SETTINGS.PROTOCOL,
    -                 hostname=CTRL_SETTINGS.HOSTNAME,
    -                 port=CTRL_SETTINGS.COMMANDING_PORT):
    -        super().__init__(connect_address(protocol, hostname, port))
    +
    class DigaloxProxy(Proxy, DigaloxInterface):
    +    def __init__(self):
    +        super().__init__(connect_address(CTRL_SETTINGS.PROTOCOL, 
    +                                         CTRL_SETTINGS.HOSTNAME, 
    +                                         CTRL_SETTINGS.COMMANDING_PORT), 
    +                         timeout=20000)

    Ancestors

    -
  • PidInterface: +
  • DigaloxInterface:
  • -
    -class PidSimulator +
    +class DigaloxSimulator
    -

    PID simulator class.

    +

    Digalox LN2 level monitor Interface base class.

    Expand source code -
    class PidSimulator(PidInterface):
    -   """ PID simulator class. """
    -
    -   def __init__(self):
    -       self._is_connected = True
    -       seed()
    -       self.running = [1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0]
    -       self.setpoints = [randint(-180, +250) for x in range(0, 14)]
    -       self.temperature = [randint(-180, +250) for x in range(0, 14)]
    -       self.timestamp =  [0 for x in range(0, 14)]
    -       self.errors   = [randint(0, 200) for x in range(0, 14)]
    -       self.isum = [randint(0, 900) for x in range(0, 14)]
    -       self.inputs = [randint(-180, +250) for x in range(0, 14)]
    -       self.outputs = [randint(-180, +250) for x in range(0, 14)]
    -
    +
    class DigaloxSimulator(DigaloxInterface):
    +    def __init__(self):
    +        self._is_connected = True
     
    -
    -   def is_connected(self):
    +    def is_connected(self):
            return self._is_connected
     
    -   def is_simulator(self):
    +    def is_simulator(self):
            return True
     
    -   def connect(self):
    +    def connect(self):
            self._is_connected = True
     
    -   def disconnect(self):
    +    def disconnect(self):
            self._is_connected = False
     
    -   def reconnect(self):
    +    def reconnect(self):
            if self.is_connected():
                self.disconnect()
            self.connect()
     
    -   def set_temperature(self, heater_idx, setpoint_temperature):
    -       logger.info("Settings temperature forPID {heater_idx} to {setpoint}")
    -       self.setpoints[heater_idx] = setpoint_temperature
    -       self.temperature[heater_idx] = setpoint_temperature - 10
    -
    -   def enable(self, channel):
    -       logger.info(f"Enabling {channel}")
    -       self.running[channel] = 1
    -
    -   def disable(self, channel):
    -       logger.info(f"Disabling {channel}")
    -       self.running[channel] = 0
    + def get_value(self): + return random.random()

    Ancestors

    -

    Methods

    -
    -
    -def disable(self, channel) -
    -
    -
    -
    - -Expand source code - -
    def disable(self, channel):
    -    logger.info(f"Disabling {channel}")
    -    self.running[channel] = 0
    -
    -
    -
    -def enable(self, channel) -
    -
    -
    -
    - -Expand source code - -
    def enable(self, channel):
    -    logger.info(f"Enabling {channel}")
    -    self.running[channel] = 1
    -
    -
    -

    Inherited members

    @@ -576,41 +501,34 @@

    Index

    • Super-module

    • Classes

      • -

        PidCommand

        +

        DigaloxCommand

      • -

        PidError

        +

        DigaloxController

      • -

        PidInterface

        -
      • diff --git a/docs/api/egse/tempcontrol/beaglebone/beaglebone_cs.html b/docs/api/egse/tempcontrol/digalox/digalox_cs.html similarity index 63% rename from docs/api/egse/tempcontrol/beaglebone/beaglebone_cs.html rename to docs/api/egse/tempcontrol/digalox/digalox_cs.html index f2ec67c..a695a79 100644 --- a/docs/api/egse/tempcontrol/beaglebone/beaglebone_cs.html +++ b/docs/api/egse/tempcontrol/digalox/digalox_cs.html @@ -4,7 +4,7 @@ -egse.tempcontrol.beaglebone.beaglebone_cs API documentation +egse.tempcontrol.digalox.digalox_cs API documentation @@ -19,42 +19,42 @@
        -

        Module egse.tempcontrol.beaglebone.beaglebone_cs

        +

        Module egse.tempcontrol.digalox.digalox_cs

        Expand source code -
        # Control server for the BeagleBone Black heater controller
        +
        #!/usr/bin/env python3
        +import logging
        +import multiprocessing
         
         import click
        -import logging
         import sys
        -
         import zmq
         from prometheus_client import start_http_server
         
         from egse.control import ControlServer
         from egse.settings import Settings
        -from egse.tempcontrol.beaglebone.beaglebone_protocol import BeagleboneProtocol
        -from egse.tempcontrol.beaglebone.beaglebone import BeagleboneProxy
        +from egse.tempcontrol.digalox.digalox import DigaloxProxy
        +from egse.tempcontrol.digalox.digalox_protocol import DigaloxProtocol
         
         logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL)
         
        -logger = logging.getLogger('BeagleBone Heater')
        +logger = logging.getLogger('__name__')
         
        -CTRL_SETTINGS = Settings.load("BeagleBone Heater Control Server")
        +CTRL_SETTINGS = Settings.load("Digalox LN2 level monitor Control Server")
         
         
        -class BeagleBoneControlServer(ControlServer):
        +class DigaloxControlServer(ControlServer):
         
             def __init__(self):
                 super().__init__()
         
        -        self.device_protocol = BeagleboneProtocol(self)
        +        self.device_protocol = DigaloxProtocol(self)
         
        -        self.logger.debug(f'Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}')
        +        logger.debug(f'Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}')
         
                 self.device_protocol.bind(self.dev_ctrl_cmd_sock)
         
        @@ -62,38 +62,27 @@ 

        Module egse.tempcontrol.beaglebone.beaglebone_cs< self.set_hk_delay(CTRL_SETTINGS.DELAY) - def get_communication_protocol(self): return CTRL_SETTINGS.PROTOCOL def get_commanding_port(self): - return CTRL_SETTINGS.COMMANDING_PORT + return CTRL_SETTINGS['COMMANDING_PORT'] def get_service_port(self): - return CTRL_SETTINGS.SERVICE_PORT + return CTRL_SETTINGS['SERVICE_PORT'] def get_monitoring_port(self): - return CTRL_SETTINGS.MONITORING_PORT + return CTRL_SETTINGS['MONITORING_PORT'] def get_storage_mnemonic(self): try: - return CTRL_SETTINGS.STORAGE_MNEMONIC + return CTRL_SETTINGS['STORAGE_MNEMONIC'] except: - return 'BB_HEATERS' + return f'Digalox' def before_serve(self): - start_http_server(CTRL_SETTINGS.METRICS_PORT) - - def after_serve(self): - for idx, dev in enumerate(self.device_protocol.beaglebone.beaglebone.dev_gpio_names): - self.device_protocol.beaglebone.beaglebone._dev_gpio[idx].socket.close() - - for idx, dev in enumerate(self.device_protocol.beaglebone.beaglebone.dev_pwm_names): - self.device_protocol.beaglebone.beaglebone._dev_pwm[idx].socket.close() - - for idx, dev in enumerate(self.device_protocol.beaglebone.beaglebone.dev_i2c_names): - self.device_protocol.beaglebone.beaglebone._dev_i2c[idx].socket.close() - + start_http_server(CTRL_SETTINGS['METRICS_PORT']) + @click.group() def cli(): @@ -101,14 +90,18 @@

        Module egse.tempcontrol.beaglebone.beaglebone_cs< @cli.command() -@click.option("--simulator", "--sim", is_flag=True, help="Start the BeagleBone Simulator as the backend.") +@click.option("--simulator", "--sim", is_flag=True, help="Start the Digalox Simulator as the backend.") def start(simulator): """Start the BeagleBone Control Server.""" if simulator: Settings.set_simulation_mode(True) try: - control_server = BeagleBoneControlServer() + logger.debug(f'Starting Digalox LN2 level monitor Control Server') + + multiprocessing.current_process().name = "digalox" + + control_server = DigaloxControlServer() control_server.serve() except KeyboardInterrupt: @@ -120,7 +113,7 @@

        Module egse.tempcontrol.beaglebone.beaglebone_cs< except Exception: - logger.exception("Cannot start the BeagleBone Control Server") + logger.exception("Cannot start the Digalox LN2 level monitor Control Server") # The above line does exactly the same as the traceback, but on the logger # import traceback # traceback.print_exc(file=sys.stdout) @@ -129,10 +122,10 @@

        Module egse.tempcontrol.beaglebone.beaglebone_cs< @cli.command() -def stop(): +def stop(index): """Send a 'quit_server' command to the Control Server.""" - with BeagleboneProxy() as proxy: + with DigaloxProxy(index) as proxy: sp = proxy.get_service_proxy() sp.quit_server() @@ -152,8 +145,8 @@

        Module egse.tempcontrol.beaglebone.beaglebone_cs<

        Classes

        -
        -class BeagleBoneControlServer +
        +class DigaloxControlServer

        The base class for all device control servers and for the Storage Manager and Configuration @@ -169,14 +162,14 @@

        Classes

        Expand source code -
        class BeagleBoneControlServer(ControlServer):
        +
        class DigaloxControlServer(ControlServer):
         
             def __init__(self):
                 super().__init__()
         
        -        self.device_protocol = BeagleboneProtocol(self)
        +        self.device_protocol = DigaloxProtocol(self)
         
        -        self.logger.debug(f'Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}')
        +        logger.debug(f'Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}')
         
                 self.device_protocol.bind(self.dev_ctrl_cmd_sock)
         
        @@ -184,37 +177,26 @@ 

        Classes

        self.set_hk_delay(CTRL_SETTINGS.DELAY) - def get_communication_protocol(self): return CTRL_SETTINGS.PROTOCOL def get_commanding_port(self): - return CTRL_SETTINGS.COMMANDING_PORT + return CTRL_SETTINGS['COMMANDING_PORT'] def get_service_port(self): - return CTRL_SETTINGS.SERVICE_PORT + return CTRL_SETTINGS['SERVICE_PORT'] def get_monitoring_port(self): - return CTRL_SETTINGS.MONITORING_PORT + return CTRL_SETTINGS['MONITORING_PORT'] def get_storage_mnemonic(self): try: - return CTRL_SETTINGS.STORAGE_MNEMONIC + return CTRL_SETTINGS['STORAGE_MNEMONIC'] except: - return 'BB_HEATERS' + return f'Digalox' def before_serve(self): - start_http_server(CTRL_SETTINGS.METRICS_PORT) - - def after_serve(self): - for idx, dev in enumerate(self.device_protocol.beaglebone.beaglebone.dev_gpio_names): - self.device_protocol.beaglebone.beaglebone._dev_gpio[idx].socket.close() - - for idx, dev in enumerate(self.device_protocol.beaglebone.beaglebone.dev_pwm_names): - self.device_protocol.beaglebone.beaglebone._dev_pwm[idx].socket.close() - - for idx, dev in enumerate(self.device_protocol.beaglebone.beaglebone.dev_i2c_names): - self.device_protocol.beaglebone.beaglebone._dev_i2c[idx].socket.close()
        + start_http_server(CTRL_SETTINGS['METRICS_PORT'])

        Ancestors

          @@ -222,27 +204,7 @@

          Ancestors

        Methods

        -
        -def after_serve(self) -
        -
        -
        -
        - -Expand source code - -
        def after_serve(self):
        -    for idx, dev in enumerate(self.device_protocol.beaglebone.beaglebone.dev_gpio_names):
        -        self.device_protocol.beaglebone.beaglebone._dev_gpio[idx].socket.close()
        -    
        -    for idx, dev in enumerate(self.device_protocol.beaglebone.beaglebone.dev_pwm_names):
        -        self.device_protocol.beaglebone.beaglebone._dev_pwm[idx].socket.close()
        -     
        -    for idx, dev in enumerate(self.device_protocol.beaglebone.beaglebone.dev_i2c_names):
        -        self.device_protocol.beaglebone.beaglebone._dev_i2c[idx].socket.close()
        -
        -
        -
        +
        def before_serve(self)
        @@ -252,10 +214,10 @@

        Methods

        Expand source code
        def before_serve(self):
        -    start_http_server(CTRL_SETTINGS.METRICS_PORT)
        + start_http_server(CTRL_SETTINGS['METRICS_PORT'])
        -
        +
        def get_commanding_port(self)
        @@ -265,10 +227,10 @@

        Methods

        Expand source code
        def get_commanding_port(self):
        -    return CTRL_SETTINGS.COMMANDING_PORT
        + return CTRL_SETTINGS['COMMANDING_PORT']
        -
        +
        def get_communication_protocol(self)
        @@ -281,7 +243,7 @@

        Methods

        return CTRL_SETTINGS.PROTOCOL
        -
        +
        def get_monitoring_port(self)
        @@ -291,10 +253,10 @@

        Methods

        Expand source code
        def get_monitoring_port(self):
        -    return CTRL_SETTINGS.MONITORING_PORT
        + return CTRL_SETTINGS['MONITORING_PORT']
        -
        +
        def get_service_port(self)
        @@ -304,10 +266,10 @@

        Methods

        Expand source code
        def get_service_port(self):
        -    return CTRL_SETTINGS.SERVICE_PORT
        + return CTRL_SETTINGS['SERVICE_PORT']
        -
        +
        def get_storage_mnemonic(self)
        @@ -318,9 +280,9 @@

        Methods

        def get_storage_mnemonic(self):
             try:
        -        return CTRL_SETTINGS.STORAGE_MNEMONIC
        +        return CTRL_SETTINGS['STORAGE_MNEMONIC']
             except:
        -        return 'BB_HEATERS'
        + return f'Digalox'
        @@ -328,10 +290,15 @@

        Inherited members

        • ControlServer:
        • @@ -348,21 +315,20 @@

          Index

          • Super-module

          • Classes

            diff --git a/docs/api/egse/tempcontrol/beaglebone/beaglebone_protocol.html b/docs/api/egse/tempcontrol/digalox/digalox_protocol.html similarity index 58% rename from docs/api/egse/tempcontrol/beaglebone/beaglebone_protocol.html rename to docs/api/egse/tempcontrol/digalox/digalox_protocol.html index cad6e18..5f02530 100644 --- a/docs/api/egse/tempcontrol/beaglebone/beaglebone_protocol.html +++ b/docs/api/egse/tempcontrol/digalox/digalox_protocol.html @@ -4,7 +4,7 @@ -egse.tempcontrol.beaglebone.beaglebone_protocol API documentation +egse.tempcontrol.digalox.digalox_protocol API documentation @@ -19,7 +19,7 @@
            -

            Module egse.tempcontrol.beaglebone.beaglebone_protocol

            +

            Module egse.tempcontrol.digalox.digalox_protocol

            @@ -28,86 +28,59 @@

            Module egse.tempcontrol.beaglebone.beaglebone_protocol
            import logging
             
            +import numpy as np
             from prometheus_client import Gauge
             
            -from egse.control import ControlServer
             from egse.protocol import CommandProtocol
             from egse.settings import Settings
            -from egse.tempcontrol.beaglebone.beaglebone import BeagleboneController
            -from egse.tempcontrol.beaglebone.beaglebone import BeagleboneInterface
            -from egse.tempcontrol.beaglebone.beaglebone import BeagleboneSimulator
            -from egse.tempcontrol.beaglebone.beaglebone_devif import BeagleboneCommand
             from egse.system import format_datetime
            +from egse.tempcontrol.digalox.digalox import DigaloxController, DigaloxInterface, DigaloxSimulator, DigaloxCommand
             from egse.zmq_ser import bind_address
             
            -COMMAND_SETTINGS = Settings.load(filename='beaglebone.yaml')
            -
             logger = logging.getLogger(__name__)
             
            +COMMAND_SETTINGS = Settings.load(filename='digalox.yaml')
            +
            +
            +class DigaloxProtocol(CommandProtocol):
            +    def __init__(self, control_server):
             
            -class BeagleboneProtocol(CommandProtocol):
            -    def __init__(self, control_server:ControlServer):
                     super().__init__()
            +
                     self.control_server = control_server
            +        self.cs_storage_mnemonic = self.control_server.get_storage_mnemonic()
             
                     if Settings.simulation_mode():
            -            self.beaglebone = BeagleboneSimulator()
            -            self._num_dev = 6
            +            self.digalox = DigaloxSimulator()
                     else:
            -            self.beaglebone = BeagleboneController()
            -            self._num_dev = len(self.beaglebone.beaglebone.dev_pwm_names)
            +            self.digalox = DigaloxController()
            +
            +        self.load_commands(COMMAND_SETTINGS.Commands, DigaloxCommand, DigaloxInterface)
             
            -        self.temperature_gauges = [Gauge(f'GSRON_HTR_TEMP_{device}', '') for device in range(self._num_dev)]
            -        self.current_gauges     = [[Gauge(f"GSRON_HTR_I_{device}_{channel}", '') for channel in range(4)] for device in range(self._num_dev)]
            -        self.voltage_gauges     = [[Gauge(f"GSRON_HTR_V_{device}_{channel}", '') for channel in range(4)] for device in range(self._num_dev)]
            -        self.resistance_gauges  = [[Gauge(f"GSRON_HTR_R_{device}_{channel}", '') for channel in range(4)] for device in range(self._num_dev)]
            -        self.power_gauges       = [[Gauge(f"GSRON_HTR_P_{device}_{channel}", '') for channel in range(4)] for device in range(self._num_dev)]
            +        self.build_device_method_lookup_table(self.digalox)
                     
            -        self.load_commands(COMMAND_SETTINGS.Commands, BeagleboneCommand, BeagleboneInterface)
            +        self.ln2_gauge = Gauge('GSRON_DIGALOX_LN2_LEVEL', '')
             
            -        self.build_device_method_lookup_table(self.beaglebone)
             
                 def get_bind_address(self):
                     return bind_address(self.control_server.get_communication_protocol(),
                                         self.control_server.get_commanding_port())
             
                 def get_status(self):
            -        status_info = super().get_status()
            -
            -        channel_enabled     = [[self.beaglebone.get_enable(htr, ch) for ch in range(0, 4)] for htr in range(0, 6)]
            -        duty_cycle          = [[self.beaglebone.get_duty_cycle(htr, ch) for ch in range(0, 4)] for htr in range(0, 6)]
            -    
            -        status_info['Enabled']      = channel_enabled
            -        status_info['duty_cycle']   = duty_cycle
            +        return super().get_status()
             
            -        return status_info
             
            -    
                 def get_housekeeping(self) -> dict:
             
                     hk_dict = {'timestamp': format_datetime()}
            -
                     try:
            -            for device in range(self._num_dev):
            -
            -                hk_dict[f"GSRON_HTR_TEMP_{device}"] = self.beaglebone.get_temperature(device)
            -                self.temperature_gauges[device].set(hk_dict[f"GSRON_HTR_TEMP_{device}"])
            -
            -                for channel in range(4):
            -                    hk_dict[f"GSRON_HTR_V_{device}_{channel}"] = self.beaglebone.get_voltage(device, channel)
            -                    hk_dict[f"GSRON_HTR_I_{device}_{channel}"] = self.beaglebone.get_current(device, channel)
            -                    hk_dict[f"GSRON_HTR_R_{device}_{channel}"] = hk_dict[f"GSRON_HTR_V_{device}_{channel}"] / hk_dict[f"GSRON_HTR_I_{device}_{channel}"] \
            -                                                                        if hk_dict[f"GSRON_HTR_I_{device}_{channel}"] else 0
            -                    hk_dict[f"GSRON_HTR_P_{device}_{channel}"] = hk_dict[f"GSRON_HTR_V_{device}_{channel}"] * hk_dict[f"GSRON_HTR_I_{device}_{channel}"]
            -                    
            -                    self.voltage_gauges[device][channel].set(hk_dict[f"GSRON_HTR_V_{device}_{channel}"])
            -                    self.current_gauges[device][channel].set(hk_dict[f"GSRON_HTR_I_{device}_{channel}"])
            -                    self.resistance_gauges[device][channel].set(hk_dict[f"GSRON_HTR_R_{device}_{channel}"])
            -                    self.power_gauges[device][channel].set(hk_dict[f"GSRON_HTR_P_{device}_{channel}"])
            -
            -        except Exception as exc:
            -            logger.warning(f'failed to get HK ({exc})')
            -
            +            hk_dict['GSRON_DIGALOX_LN2_LEVEL'] = self.digalox.get_value()
            +        except Exception as ex:
            +            logger.warning(f"Could not retrieve LN2 level housekeeping: {ex}")
            +            hk_dict['GSRON_DIGALOX_LN2_LEVEL'] = np.nan
            +        
            +        self.ln2_gauge.set(hk_dict['GSRON_DIGALOX_LN2_LEVEL'])
            +        
                     return hk_dict

            @@ -120,9 +93,9 @@

            Module egse.tempcontrol.beaglebone.beaglebone_protocol

            Classes

            -
            -class BeagleboneProtocol -(control_server: ControlServer) +
            +class DigaloxProtocol +(control_server)

            This class is the glue between the control servers and the hardware @@ -137,69 +110,45 @@

            Classes

            Expand source code -
            class BeagleboneProtocol(CommandProtocol):
            -    def __init__(self, control_server:ControlServer):
            +
            class DigaloxProtocol(CommandProtocol):
            +    def __init__(self, control_server):
            +
                     super().__init__()
            +
                     self.control_server = control_server
            +        self.cs_storage_mnemonic = self.control_server.get_storage_mnemonic()
             
                     if Settings.simulation_mode():
            -            self.beaglebone = BeagleboneSimulator()
            -            self._num_dev = 6
            +            self.digalox = DigaloxSimulator()
                     else:
            -            self.beaglebone = BeagleboneController()
            -            self._num_dev = len(self.beaglebone.beaglebone.dev_pwm_names)
            +            self.digalox = DigaloxController()
             
            -        self.temperature_gauges = [Gauge(f'GSRON_HTR_TEMP_{device}', '') for device in range(self._num_dev)]
            -        self.current_gauges     = [[Gauge(f"GSRON_HTR_I_{device}_{channel}", '') for channel in range(4)] for device in range(self._num_dev)]
            -        self.voltage_gauges     = [[Gauge(f"GSRON_HTR_V_{device}_{channel}", '') for channel in range(4)] for device in range(self._num_dev)]
            -        self.resistance_gauges  = [[Gauge(f"GSRON_HTR_R_{device}_{channel}", '') for channel in range(4)] for device in range(self._num_dev)]
            -        self.power_gauges       = [[Gauge(f"GSRON_HTR_P_{device}_{channel}", '') for channel in range(4)] for device in range(self._num_dev)]
            +        self.load_commands(COMMAND_SETTINGS.Commands, DigaloxCommand, DigaloxInterface)
            +
            +        self.build_device_method_lookup_table(self.digalox)
                     
            -        self.load_commands(COMMAND_SETTINGS.Commands, BeagleboneCommand, BeagleboneInterface)
            +        self.ln2_gauge = Gauge('GSRON_DIGALOX_LN2_LEVEL', '')
             
            -        self.build_device_method_lookup_table(self.beaglebone)
             
                 def get_bind_address(self):
                     return bind_address(self.control_server.get_communication_protocol(),
                                         self.control_server.get_commanding_port())
             
                 def get_status(self):
            -        status_info = super().get_status()
            -
            -        channel_enabled     = [[self.beaglebone.get_enable(htr, ch) for ch in range(0, 4)] for htr in range(0, 6)]
            -        duty_cycle          = [[self.beaglebone.get_duty_cycle(htr, ch) for ch in range(0, 4)] for htr in range(0, 6)]
            -    
            -        status_info['Enabled']      = channel_enabled
            -        status_info['duty_cycle']   = duty_cycle
            +        return super().get_status()
             
            -        return status_info
             
            -    
                 def get_housekeeping(self) -> dict:
             
                     hk_dict = {'timestamp': format_datetime()}
            -
                     try:
            -            for device in range(self._num_dev):
            -
            -                hk_dict[f"GSRON_HTR_TEMP_{device}"] = self.beaglebone.get_temperature(device)
            -                self.temperature_gauges[device].set(hk_dict[f"GSRON_HTR_TEMP_{device}"])
            -
            -                for channel in range(4):
            -                    hk_dict[f"GSRON_HTR_V_{device}_{channel}"] = self.beaglebone.get_voltage(device, channel)
            -                    hk_dict[f"GSRON_HTR_I_{device}_{channel}"] = self.beaglebone.get_current(device, channel)
            -                    hk_dict[f"GSRON_HTR_R_{device}_{channel}"] = hk_dict[f"GSRON_HTR_V_{device}_{channel}"] / hk_dict[f"GSRON_HTR_I_{device}_{channel}"] \
            -                                                                        if hk_dict[f"GSRON_HTR_I_{device}_{channel}"] else 0
            -                    hk_dict[f"GSRON_HTR_P_{device}_{channel}"] = hk_dict[f"GSRON_HTR_V_{device}_{channel}"] * hk_dict[f"GSRON_HTR_I_{device}_{channel}"]
            -                    
            -                    self.voltage_gauges[device][channel].set(hk_dict[f"GSRON_HTR_V_{device}_{channel}"])
            -                    self.current_gauges[device][channel].set(hk_dict[f"GSRON_HTR_I_{device}_{channel}"])
            -                    self.resistance_gauges[device][channel].set(hk_dict[f"GSRON_HTR_R_{device}_{channel}"])
            -                    self.power_gauges[device][channel].set(hk_dict[f"GSRON_HTR_P_{device}_{channel}"])
            -
            -        except Exception as exc:
            -            logger.warning(f'failed to get HK ({exc})')
            -
            +            hk_dict['GSRON_DIGALOX_LN2_LEVEL'] = self.digalox.get_value()
            +        except Exception as ex:
            +            logger.warning(f"Could not retrieve LN2 level housekeeping: {ex}")
            +            hk_dict['GSRON_DIGALOX_LN2_LEVEL'] = np.nan
            +        
            +        self.ln2_gauge.set(hk_dict['GSRON_DIGALOX_LN2_LEVEL'])
            +        
                     return hk_dict

            Ancestors

            @@ -240,13 +189,13 @@

            Index

            diff --git a/docs/api/egse/tempcontrol/index.html b/docs/api/egse/tempcontrol/index.html index 35e8c32..6b19199 100644 --- a/docs/api/egse/tempcontrol/index.html +++ b/docs/api/egse/tempcontrol/index.html @@ -46,7 +46,17 @@

            Module egse.tempcontrol

            """ class TempError(Exception): - pass
            + pass + +# The __pdoc__ dict is understood by pdoc3 and instructs to exclude the 'keys' from the documentation. +# See: https://pdoc3.github.io/pdoc/doc/pdoc/#overriding-docstrings-with-__pdoc__ + +# The following modules are excluded because they assume the gssw package is installed (SRON specific) + +__pdoc__ = { + 'beaglebone': False, + 'spid': False, +}

        @@ -56,7 +66,7 @@

        Sub-modules

        -
        egse.tempcontrol.beaglebone
        +
        egse.tempcontrol.digalox
        @@ -72,10 +82,6 @@

        Sub-modules

        -
        egse.tempcontrol.spid
        -
        -
        -
        egse.tempcontrol.srs

        Device control for the SRS/PTC10 that controls the SRS temperature control system of the MaRi …

        @@ -125,11 +131,10 @@

        Index

      • Sub-modules

      • diff --git a/docs/api/egse/tempcontrol/keithley/daq6510_cs.html b/docs/api/egse/tempcontrol/keithley/daq6510_cs.html index ad4d5ca..5dd1ef7 100644 --- a/docs/api/egse/tempcontrol/keithley/daq6510_cs.html +++ b/docs/api/egse/tempcontrol/keithley/daq6510_cs.html @@ -396,10 +396,15 @@

        Inherited members

        diff --git a/docs/api/egse/tempcontrol/lakeshore/lsci.html b/docs/api/egse/tempcontrol/lakeshore/lsci.html new file mode 100644 index 0000000..5ad9eb8 --- /dev/null +++ b/docs/api/egse/tempcontrol/lakeshore/lsci.html @@ -0,0 +1,1373 @@ + + + + + + +egse.tempcontrol.lakeshore.lsci API documentation + + + + + + + + + + + +
        +
        +
        +

        Module egse.tempcontrol.lakeshore.lsci

        +
        +
        +

        Define the basic classes to access the LakeShore devices.

        +
        + +Expand source code + +
        """
        +Define the basic classes to access the LakeShore devices.
        +"""
        +
        +import logging
        +
        +from egse.decorators import dynamic_interface
        +from egse.device import DeviceInterface
        +from egse.proxy import Proxy
        +from egse.randomwalk import RandomWalk
        +from egse.settings import Settings
        +from egse.tempcontrol.lakeshore.lsci_devif import LakeShoreEthernetInterface
        +from egse.tempcontrol.lakeshore.lsci_devif import LakeShoreError
        +from egse.zmq_ser import connect_address
        +
        +logger = logging.getLogger(__name__)
        +
        +CTRL_SETTINGS = Settings.load("LakeShore Control Server")
        +LAKESHORE_SETTINGS = Settings.load("LakeShore Controller")
        +DEVICE_SETTINGS = Settings.load(filename="lsci.yaml")
        +
        +
        +class LakeShoreInterface(DeviceInterface):
        +    """
        +    The LakeShore base class.
        +    """
        +    @dynamic_interface
        +    def info(self) -> str:
        +        """
        +        Retrieve basic information about the LakeShore controller.
        +        :return: a multiline string with information about the device
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_id(self) -> str:
        +        """
        +        Returns lakeshore ID.
        +
        +        Returns:
        +            The current ID: LS_X when X is the index of LakeShore.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_temperature(self) -> float:
        +        """
        +        Retrieve the temperature from the given channel.
        +
        +        Returns:
        +            The current temperature for the given channel.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_params_pid(self, output:int) -> list:
        +        """
        +        Retrieve the PID Params.
        +
        +        Returns:
        +            The params of P, I, D:
        +            Pos[0]: P
        +            Pos[1]: I
        +            Pos[2]: D
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_setpoint(self, output:int) -> float:
        +        """
        +        Retrieve the Setpoint Params.
        +
        +        Returns:
        +            The setpoint:
        +        """
        +        raise NotImplementedError
        +    
        +    @dynamic_interface
        +    def get_tuning_status(self) -> list:
        +        """
        +        Returns the remote interface mode.
        +
        +        Returns:
        +            Pos[0]: tuning status:  0 = no active tuning, 1 = active tuning.
        +            Pos[1]: output:         1 = output 1, 2 = output 2
        +            Pos[2]: error status:   0 = no tuning error, 1 = tuning error
        +            Pos[3]: stage status:   Specifies the current stage in the Autotune process.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_heater_setup(self, output:int) -> list:
        +        """
        +        Return the Heater Setup Params.
        +
        +        Returns:
        +            The params of Heater setup:
        +            Pos[0]: htr resistance
        +            Pos[1]: max current
        +            Pos[2]: max user current
        +            Pos[3]: current/power.
        +        """
        +        raise NotImplementedError
        +    
        +    @dynamic_interface
        +    def get_range(self, output:int) -> int:
        +        """
        +        Return the Range Params.
        +
        +        Returns:
        +            The params of Range: 0 = Off, 1 = Low, 2 = Medium, 3 = High.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_heater_status(self, output:int) -> int:
        +        """
        +        Return the Heater Status.
        +
        +        Returns:
        +            The params of Heater setup: 0 = no error, 1 = heater open load, 2 = heater short
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_heater(self, output:int) -> float:
        +        """
        +        Return the Heater Value.
        +
        +        Returns:
        +            The value of Heater
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def set_params_pid(self, output:int, p:int, i:int, d:int):
        +        """
        +        Configure the PID Params.
        +        """
        +        raise NotImplementedError
        +    
        +    @dynamic_interface
        +    def set_setpoint(self, output:int, value:str):
        +        """
        +        Configure the Setpoint Params.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def set_autotune(self, output:int, mode:int):
        +        """
        +        Configure the AutoTune Params.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def set_heater_setup(self,output:int, heater_resistant:int, max_current:int, max_user_current:str, output_display:int):
        +        """
        +        Configure the Heater Params.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def set_range(self, output:int, range:int):
        +        """
        +        Configure the Range.
        +        """
        +        raise NotImplementedError
        +
        +
        +class LakeShoreSimulator(LakeShoreInterface):
        +    """
        +    The LakeShore controller Simulator class for Model .
        +    """
        +
        +    def __init__(self):
        +        self._is_connected = True
        +        self._randomwalk = RandomWalk(start=15, boundary=(-100, 25), scale=0.01, count=0)
        +        self._p1_value = 1.2
        +        self._i1_value = 10.2
        +        self._d1_value = 50.0
        +        self._p2_value = 3.2
        +        self._i2_value = 13.2
        +        self._d2_value = 5.0
        +        self._setpoint = "125.3"
        +        self._heater_value = "25.3"
        +
        +    def is_connected(self):
        +        return self._is_connected
        +
        +    def is_simulator(self):
        +        return True
        +
        +    def connect(self):
        +        self._is_connected = True
        +
        +    def disconnect(self):
        +        self._is_connected = False
        +
        +    def reconnect(self):
        +        if self.is_connected():
        +            self.disconnect()
        +        self.connect()
        +
        +    def info(self):
        +        return "LSCI - The LakeShore Simulator"
        +
        +    def get_id(self) -> str:
        +        return "LS_X"
        +
        +    def get_temperature(self) -> float:
        +        return next(self._randomwalk)
        +
        +    def get_params_pid(self, output:int) -> list:
        +        if output == 1:
        +            return [self._p1_value,self._i1_value,self._d1_value]
        +        elif output == 2:
        +            return [self._p2_value,self._i2_value,self._d2_value]
        +        else:
        +            return [0.0,0.0,0.0]
        +    
        +    def set_params_pid(self, output:int, p:int, i:int, d:int):
        +        if output == 1:
        +            self._p1_value = p
        +            self._i1_value = i
        +            self._d1_value = d
        +        elif output == 2:
        +            self._p2_value = p
        +            self._i2_value = i
        +            self._d2_value = d
        +        else:
        +            return
        +
        +    def set_setpoint(self, output:int, value:str):
        +        self._setpoint = value
        +    
        +    def get_setpoint(self, output:int):
        +        return self._setpoint
        +
        +    def get_heater(self, output:int):
        +        return self._heater_value
        +
        +
        +class LakeShoreController(LakeShoreInterface):
        +    """
        +    The LakeShore Controller class for Model .
        +    """
        +
        +    def __init__(self, device_index):
        +        """Initialize the LakeShoreController interface."""
        +
        +        super().__init__()
        +
        +        device_id = f"LS_{device_index}"
        +        logger.debug(f"Initializing LakeShoreController {device_id} Model: 336")
        +        try:
        +            self.ls = LakeShoreEthernetInterface(device_index)
        +        except LakeShoreError as exc:
        +            logger.warning(f"LakeShoreError caught: Couldn't establish connection ({exc})")
        +            raise LakeShoreError(
        +                "Couldn't establish a connection with the LakeShore controller."
        +            ) from exc
        +
        +    def connect(self):
        +        try:
        +            self.ls.connect()
        +        except LakeShoreError as exc:
        +            logger.warning(f"LakeShoreError caught: Couldn't establish connection ({exc})")
        +            raise ConnectionError(
        +                "Couldn't establish a connection with the LakeShore Controller."
        +            ) from exc
        +
        +    def disconnect(self):
        +        try:
        +            self.ls.disconnect()
        +        except LakeShoreError as exc:
        +            raise ConnectionError("Couldn't disconnect from LakeShore Controller.") from exc
        +
        +    def reconnect(self):
        +        if self.is_connected():
        +            self.disconnect()
        +        self.connect()
        +
        +    def is_simulator(self):
        +        return False
        +
        +    def is_connected(self):
        +        """Check if the LakeShore Controller is connected. """
        +        return self.ls.is_connected()
        +
        +    def info(self) -> str:
        +        return self.ls.info()
        +
        +    def get_id(self) -> str:
        +        return self.ls.get_id()
        +
        +    def get_temperature(self) -> float:
        +        return self.ls.get_temperature()
        +
        +    def get_params_pid(self, output:int) -> list:
        +        return self.ls.get_params_pid(output)
        +
        +    def get_setpoint(self, output:int) -> float:
        +        return self.ls.get_setpoint(output)
        +    
        +    def get_tuning_status(self) -> list:
        +        return self.ls.get_tuning_status()
        +
        +    def get_heater_setup(self, output:int) -> list:
        +        return self.ls.get_heater_setup(output)
        +
        +    def get_heater_status(self, output:int) -> int:
        +        return self.ls.get_heater_status(output)
        +
        +    def get_heater(self, output:int) -> float:
        +        return self.ls.get_heater(output)
        +
        +    def get_range(self, output:int) -> int:
        +        return self.ls.get_range(output)
        +
        +    def set_params_pid(self, output:int, p:int, i:int, d:int):
        +        self.ls.set_params_pid(output, p, i, d)
        +
        +    def set_setpoint(self, output:int, value:str):
        +        self.ls.set_setpoint(output, value)
        +
        +    def set_autotune(self, output:int, mode:int):
        +        self.ls.set_autotune(output, mode)
        +
        +    def set_heater_setup(self,output:int, heater_resistant:int, max_current:int, max_user_current:str, output_display:int):
        +        self.ls.set_heater_setup(output, heater_resistant, max_current, max_user_current, output_display)
        +
        +    def set_range(self, output:int, range:int):
        +        self.ls.set_range(output, range)
        +
        +
        +    def _get_response(self, cmd_string):
        +        response = self.ls.get_response(cmd_string)
        +        return response
        +
        +
        +class LakeShoreProxy(Proxy, LakeShoreInterface):
        +    """
        +    The LakeShoreProxy class is used to connect to the LakeShore control server and send commands
        +    to the LakeShore Hardware Controller remotely.
        +    """
        +    def __init__(self, lsci_index: int):
        +        self.name = "LS_"+str(lsci_index)
        +        protocol=CTRL_SETTINGS.PROTOCOL
        +        hostname=CTRL_SETTINGS.HOSTNAME
        +        port=CTRL_SETTINGS[self.name]["COMMANDING_PORT"]
        +        """
        +        Args:
        +            protocol: the transport protocol [default is taken from settings file]
        +            hostname: location of the control server (IP address)
        +                [default is taken from settings file]
        +            port: TCP port on which the control server is listening for commands
        +                [default is taken from settings file]
        +        """
        +        super().__init__(connect_address(protocol, hostname, port))
        + 
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +

        Classes

        +
        +
        +class LakeShoreController +(device_index) +
        +
        +

        The LakeShore Controller class for Model .

        +

        Initialize the LakeShoreController interface.

        +
        + +Expand source code + +
        class LakeShoreController(LakeShoreInterface):
        +    """
        +    The LakeShore Controller class for Model .
        +    """
        +
        +    def __init__(self, device_index):
        +        """Initialize the LakeShoreController interface."""
        +
        +        super().__init__()
        +
        +        device_id = f"LS_{device_index}"
        +        logger.debug(f"Initializing LakeShoreController {device_id} Model: 336")
        +        try:
        +            self.ls = LakeShoreEthernetInterface(device_index)
        +        except LakeShoreError as exc:
        +            logger.warning(f"LakeShoreError caught: Couldn't establish connection ({exc})")
        +            raise LakeShoreError(
        +                "Couldn't establish a connection with the LakeShore controller."
        +            ) from exc
        +
        +    def connect(self):
        +        try:
        +            self.ls.connect()
        +        except LakeShoreError as exc:
        +            logger.warning(f"LakeShoreError caught: Couldn't establish connection ({exc})")
        +            raise ConnectionError(
        +                "Couldn't establish a connection with the LakeShore Controller."
        +            ) from exc
        +
        +    def disconnect(self):
        +        try:
        +            self.ls.disconnect()
        +        except LakeShoreError as exc:
        +            raise ConnectionError("Couldn't disconnect from LakeShore Controller.") from exc
        +
        +    def reconnect(self):
        +        if self.is_connected():
        +            self.disconnect()
        +        self.connect()
        +
        +    def is_simulator(self):
        +        return False
        +
        +    def is_connected(self):
        +        """Check if the LakeShore Controller is connected. """
        +        return self.ls.is_connected()
        +
        +    def info(self) -> str:
        +        return self.ls.info()
        +
        +    def get_id(self) -> str:
        +        return self.ls.get_id()
        +
        +    def get_temperature(self) -> float:
        +        return self.ls.get_temperature()
        +
        +    def get_params_pid(self, output:int) -> list:
        +        return self.ls.get_params_pid(output)
        +
        +    def get_setpoint(self, output:int) -> float:
        +        return self.ls.get_setpoint(output)
        +    
        +    def get_tuning_status(self) -> list:
        +        return self.ls.get_tuning_status()
        +
        +    def get_heater_setup(self, output:int) -> list:
        +        return self.ls.get_heater_setup(output)
        +
        +    def get_heater_status(self, output:int) -> int:
        +        return self.ls.get_heater_status(output)
        +
        +    def get_heater(self, output:int) -> float:
        +        return self.ls.get_heater(output)
        +
        +    def get_range(self, output:int) -> int:
        +        return self.ls.get_range(output)
        +
        +    def set_params_pid(self, output:int, p:int, i:int, d:int):
        +        self.ls.set_params_pid(output, p, i, d)
        +
        +    def set_setpoint(self, output:int, value:str):
        +        self.ls.set_setpoint(output, value)
        +
        +    def set_autotune(self, output:int, mode:int):
        +        self.ls.set_autotune(output, mode)
        +
        +    def set_heater_setup(self,output:int, heater_resistant:int, max_current:int, max_user_current:str, output_display:int):
        +        self.ls.set_heater_setup(output, heater_resistant, max_current, max_user_current, output_display)
        +
        +    def set_range(self, output:int, range:int):
        +        self.ls.set_range(output, range)
        +
        +
        +    def _get_response(self, cmd_string):
        +        response = self.ls.get_response(cmd_string)
        +        return response
        +
        +

        Ancestors

        + +

        Methods

        +
        +
        +def is_connected(self) +
        +
        +

        Check if the LakeShore Controller is connected.

        +
        + +Expand source code + +
        def is_connected(self):
        +    """Check if the LakeShore Controller is connected. """
        +    return self.ls.is_connected()
        +
        +
        +
        +

        Inherited members

        + +
        +
        +class LakeShoreInterface +
        +
        +

        The LakeShore base class.

        +
        + +Expand source code + +
        class LakeShoreInterface(DeviceInterface):
        +    """
        +    The LakeShore base class.
        +    """
        +    @dynamic_interface
        +    def info(self) -> str:
        +        """
        +        Retrieve basic information about the LakeShore controller.
        +        :return: a multiline string with information about the device
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_id(self) -> str:
        +        """
        +        Returns lakeshore ID.
        +
        +        Returns:
        +            The current ID: LS_X when X is the index of LakeShore.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_temperature(self) -> float:
        +        """
        +        Retrieve the temperature from the given channel.
        +
        +        Returns:
        +            The current temperature for the given channel.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_params_pid(self, output:int) -> list:
        +        """
        +        Retrieve the PID Params.
        +
        +        Returns:
        +            The params of P, I, D:
        +            Pos[0]: P
        +            Pos[1]: I
        +            Pos[2]: D
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_setpoint(self, output:int) -> float:
        +        """
        +        Retrieve the Setpoint Params.
        +
        +        Returns:
        +            The setpoint:
        +        """
        +        raise NotImplementedError
        +    
        +    @dynamic_interface
        +    def get_tuning_status(self) -> list:
        +        """
        +        Returns the remote interface mode.
        +
        +        Returns:
        +            Pos[0]: tuning status:  0 = no active tuning, 1 = active tuning.
        +            Pos[1]: output:         1 = output 1, 2 = output 2
        +            Pos[2]: error status:   0 = no tuning error, 1 = tuning error
        +            Pos[3]: stage status:   Specifies the current stage in the Autotune process.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_heater_setup(self, output:int) -> list:
        +        """
        +        Return the Heater Setup Params.
        +
        +        Returns:
        +            The params of Heater setup:
        +            Pos[0]: htr resistance
        +            Pos[1]: max current
        +            Pos[2]: max user current
        +            Pos[3]: current/power.
        +        """
        +        raise NotImplementedError
        +    
        +    @dynamic_interface
        +    def get_range(self, output:int) -> int:
        +        """
        +        Return the Range Params.
        +
        +        Returns:
        +            The params of Range: 0 = Off, 1 = Low, 2 = Medium, 3 = High.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_heater_status(self, output:int) -> int:
        +        """
        +        Return the Heater Status.
        +
        +        Returns:
        +            The params of Heater setup: 0 = no error, 1 = heater open load, 2 = heater short
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def get_heater(self, output:int) -> float:
        +        """
        +        Return the Heater Value.
        +
        +        Returns:
        +            The value of Heater
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def set_params_pid(self, output:int, p:int, i:int, d:int):
        +        """
        +        Configure the PID Params.
        +        """
        +        raise NotImplementedError
        +    
        +    @dynamic_interface
        +    def set_setpoint(self, output:int, value:str):
        +        """
        +        Configure the Setpoint Params.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def set_autotune(self, output:int, mode:int):
        +        """
        +        Configure the AutoTune Params.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def set_heater_setup(self,output:int, heater_resistant:int, max_current:int, max_user_current:str, output_display:int):
        +        """
        +        Configure the Heater Params.
        +        """
        +        raise NotImplementedError
        +
        +    @dynamic_interface
        +    def set_range(self, output:int, range:int):
        +        """
        +        Configure the Range.
        +        """
        +        raise NotImplementedError
        +
        +

        Ancestors

        + +

        Subclasses

        + +

        Methods

        +
        +
        +def get_heater(self, output: int) ‑> float +
        +
        +

        Return the Heater Value.

        +

        Returns

        +

        The value of Heater

        +
        + +Expand source code + +
        @dynamic_interface
        +def get_heater(self, output:int) -> float:
        +    """
        +    Return the Heater Value.
        +
        +    Returns:
        +        The value of Heater
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def get_heater_setup(self, output: int) ‑> list +
        +
        +

        Return the Heater Setup Params.

        +

        Returns

        +
        +
        The params of Heater setup:
        +
        Pos[0]
        +
        htr resistance
        +
        Pos[1]
        +
        max current
        +
        Pos[2]
        +
        max user current
        +
        Pos[3]
        +
        current/power.
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def get_heater_setup(self, output:int) -> list:
        +    """
        +    Return the Heater Setup Params.
        +
        +    Returns:
        +        The params of Heater setup:
        +        Pos[0]: htr resistance
        +        Pos[1]: max current
        +        Pos[2]: max user current
        +        Pos[3]: current/power.
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def get_heater_status(self, output: int) ‑> int +
        +
        +

        Return the Heater Status.

        +

        Returns

        +
        +
        The params of Heater setup
        +
        0 = no error, 1 = heater open load, 2 = heater short
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def get_heater_status(self, output:int) -> int:
        +    """
        +    Return the Heater Status.
        +
        +    Returns:
        +        The params of Heater setup: 0 = no error, 1 = heater open load, 2 = heater short
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def get_id(self) ‑> str +
        +
        +

        Returns lakeshore ID.

        +

        Returns

        +
        +
        The current ID
        +
        LS_X when X is the index of LakeShore.
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def get_id(self) -> str:
        +    """
        +    Returns lakeshore ID.
        +
        +    Returns:
        +        The current ID: LS_X when X is the index of LakeShore.
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def get_params_pid(self, output: int) ‑> list +
        +
        +

        Retrieve the PID Params.

        +

        Returns

        +
        +
        The params of P, I, D:
        +
        Pos[0]
        +
        P
        +
        Pos[1]
        +
        I
        +
        Pos[2]
        +
        D
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def get_params_pid(self, output:int) -> list:
        +    """
        +    Retrieve the PID Params.
        +
        +    Returns:
        +        The params of P, I, D:
        +        Pos[0]: P
        +        Pos[1]: I
        +        Pos[2]: D
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def get_range(self, output: int) ‑> int +
        +
        +

        Return the Range Params.

        +

        Returns

        +
        +
        The params of Range
        +
        0 = Off, 1 = Low, 2 = Medium, 3 = High.
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def get_range(self, output:int) -> int:
        +    """
        +    Return the Range Params.
        +
        +    Returns:
        +        The params of Range: 0 = Off, 1 = Low, 2 = Medium, 3 = High.
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def get_setpoint(self, output: int) ‑> float +
        +
        +

        Retrieve the Setpoint Params.

        +

        Returns

        +

        The setpoint:

        +
        + +Expand source code + +
        @dynamic_interface
        +def get_setpoint(self, output:int) -> float:
        +    """
        +    Retrieve the Setpoint Params.
        +
        +    Returns:
        +        The setpoint:
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def get_temperature(self) ‑> float +
        +
        +

        Retrieve the temperature from the given channel.

        +

        Returns

        +

        The current temperature for the given channel.

        +
        + +Expand source code + +
        @dynamic_interface
        +def get_temperature(self) -> float:
        +    """
        +    Retrieve the temperature from the given channel.
        +
        +    Returns:
        +        The current temperature for the given channel.
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def get_tuning_status(self) ‑> list +
        +
        +

        Returns the remote interface mode.

        +

        Returns

        +
        +
        Pos[0]
        +
        tuning status: +0 = no active tuning, 1 = active tuning.
        +
        Pos[1]
        +
        output: +1 = output 1, 2 = output 2
        +
        Pos[2]
        +
        error status: +0 = no tuning error, 1 = tuning error
        +
        Pos[3]
        +
        stage status: +Specifies the current stage in the Autotune process.
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def get_tuning_status(self) -> list:
        +    """
        +    Returns the remote interface mode.
        +
        +    Returns:
        +        Pos[0]: tuning status:  0 = no active tuning, 1 = active tuning.
        +        Pos[1]: output:         1 = output 1, 2 = output 2
        +        Pos[2]: error status:   0 = no tuning error, 1 = tuning error
        +        Pos[3]: stage status:   Specifies the current stage in the Autotune process.
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def info(self) ‑> str +
        +
        +

        Retrieve basic information about the LakeShore controller. +:return: a multiline string with information about the device

        +
        + +Expand source code + +
        @dynamic_interface
        +def info(self) -> str:
        +    """
        +    Retrieve basic information about the LakeShore controller.
        +    :return: a multiline string with information about the device
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def set_autotune(self, output: int, mode: int) +
        +
        +

        Configure the AutoTune Params.

        +
        + +Expand source code + +
        @dynamic_interface
        +def set_autotune(self, output:int, mode:int):
        +    """
        +    Configure the AutoTune Params.
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def set_heater_setup(self, output: int, heater_resistant: int, max_current: int, max_user_current: str, output_display: int) +
        +
        +

        Configure the Heater Params.

        +
        + +Expand source code + +
        @dynamic_interface
        +def set_heater_setup(self,output:int, heater_resistant:int, max_current:int, max_user_current:str, output_display:int):
        +    """
        +    Configure the Heater Params.
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def set_params_pid(self, output: int, p: int, i: int, d: int) +
        +
        +

        Configure the PID Params.

        +
        + +Expand source code + +
        @dynamic_interface
        +def set_params_pid(self, output:int, p:int, i:int, d:int):
        +    """
        +    Configure the PID Params.
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def set_range(self, output: int, range: int) +
        +
        +

        Configure the Range.

        +
        + +Expand source code + +
        @dynamic_interface
        +def set_range(self, output:int, range:int):
        +    """
        +    Configure the Range.
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +def set_setpoint(self, output: int, value: str) +
        +
        +

        Configure the Setpoint Params.

        +
        + +Expand source code + +
        @dynamic_interface
        +def set_setpoint(self, output:int, value:str):
        +    """
        +    Configure the Setpoint Params.
        +    """
        +    raise NotImplementedError
        +
        +
        +
        +

        Inherited members

        + +
        +
        +class LakeShoreProxy +(lsci_index: int) +
        +
        +

        The LakeShoreProxy class is used to connect to the LakeShore control server and send commands +to the LakeShore Hardware Controller remotely.

        +

        During initialization, the Proxy will connect to the control server and send a +handshaking Ping command. When that succeeds the Proxy will request and load the +available commands from the control server. When the connection with the control server +fails, no commands are loaded and the Proxy is left in a 'disconnected' state. The caller +can fix the problem with the control server and call connect_cs(), followed by a call to +load_commands().

        +

        The timeout argument specifies the number of milliseconds

        +
        + +Expand source code + +
        class LakeShoreProxy(Proxy, LakeShoreInterface):
        +    """
        +    The LakeShoreProxy class is used to connect to the LakeShore control server and send commands
        +    to the LakeShore Hardware Controller remotely.
        +    """
        +    def __init__(self, lsci_index: int):
        +        self.name = "LS_"+str(lsci_index)
        +        protocol=CTRL_SETTINGS.PROTOCOL
        +        hostname=CTRL_SETTINGS.HOSTNAME
        +        port=CTRL_SETTINGS[self.name]["COMMANDING_PORT"]
        +        """
        +        Args:
        +            protocol: the transport protocol [default is taken from settings file]
        +            hostname: location of the control server (IP address)
        +                [default is taken from settings file]
        +            port: TCP port on which the control server is listening for commands
        +                [default is taken from settings file]
        +        """
        +        super().__init__(connect_address(protocol, hostname, port))
        +
        +

        Ancestors

        + +

        Inherited members

        + +
        +
        +class LakeShoreSimulator +
        +
        +

        The LakeShore controller Simulator class for Model .

        +
        + +Expand source code + +
        class LakeShoreSimulator(LakeShoreInterface):
        +    """
        +    The LakeShore controller Simulator class for Model .
        +    """
        +
        +    def __init__(self):
        +        self._is_connected = True
        +        self._randomwalk = RandomWalk(start=15, boundary=(-100, 25), scale=0.01, count=0)
        +        self._p1_value = 1.2
        +        self._i1_value = 10.2
        +        self._d1_value = 50.0
        +        self._p2_value = 3.2
        +        self._i2_value = 13.2
        +        self._d2_value = 5.0
        +        self._setpoint = "125.3"
        +        self._heater_value = "25.3"
        +
        +    def is_connected(self):
        +        return self._is_connected
        +
        +    def is_simulator(self):
        +        return True
        +
        +    def connect(self):
        +        self._is_connected = True
        +
        +    def disconnect(self):
        +        self._is_connected = False
        +
        +    def reconnect(self):
        +        if self.is_connected():
        +            self.disconnect()
        +        self.connect()
        +
        +    def info(self):
        +        return "LSCI - The LakeShore Simulator"
        +
        +    def get_id(self) -> str:
        +        return "LS_X"
        +
        +    def get_temperature(self) -> float:
        +        return next(self._randomwalk)
        +
        +    def get_params_pid(self, output:int) -> list:
        +        if output == 1:
        +            return [self._p1_value,self._i1_value,self._d1_value]
        +        elif output == 2:
        +            return [self._p2_value,self._i2_value,self._d2_value]
        +        else:
        +            return [0.0,0.0,0.0]
        +    
        +    def set_params_pid(self, output:int, p:int, i:int, d:int):
        +        if output == 1:
        +            self._p1_value = p
        +            self._i1_value = i
        +            self._d1_value = d
        +        elif output == 2:
        +            self._p2_value = p
        +            self._i2_value = i
        +            self._d2_value = d
        +        else:
        +            return
        +
        +    def set_setpoint(self, output:int, value:str):
        +        self._setpoint = value
        +    
        +    def get_setpoint(self, output:int):
        +        return self._setpoint
        +
        +    def get_heater(self, output:int):
        +        return self._heater_value
        +
        +

        Ancestors

        + +

        Inherited members

        + +
        +
        +
        +
        + +
        + + + \ No newline at end of file diff --git a/docs/api/egse/tempcontrol/lakeshore/lsci336.html b/docs/api/egse/tempcontrol/lakeshore/lsci336.html deleted file mode 100644 index dbda802..0000000 --- a/docs/api/egse/tempcontrol/lakeshore/lsci336.html +++ /dev/null @@ -1,613 +0,0 @@ - - - - - - -egse.tempcontrol.lakeshore.lsci336 API documentation - - - - - - - - - - - -
        -
        -
        -

        Module egse.tempcontrol.lakeshore.lsci336

        -
        -
        -

        Define the basic classes to access the LakeShore devices.

        -
        - -Expand source code - -
        """
        -Define the basic classes to access the LakeShore devices.
        -"""
        -
        -import logging
        -
        -from egse.decorators import dynamic_interface
        -from egse.device import DeviceInterface
        -from egse.proxy import Proxy
        -from egse.randomwalk import RandomWalk
        -from egse.settings import Settings
        -from egse.tempcontrol.lakeshore.lsci336_devif import LakeShore336EthernetInterface
        -from egse.tempcontrol.lakeshore.lsci336_devif import LakeShoreError
        -from egse.zmq_ser import connect_address
        -
        -logger = logging.getLogger(__name__)
        -
        -CTRL_SETTINGS = Settings.load("LakeShore Control Server")
        -LAKESHORE_SETTINGS = Settings.load("LakeShore Controller")
        -DEVICE_SETTINGS = Settings.load(filename="lsci336.yaml")
        -
        -
        -class LakeShore336Interface(DeviceInterface):
        -    """
        -    The LakeShore base class.
        -    """
        -
        -    @dynamic_interface
        -    def info(self) -> str:
        -        """
        -        Retrieve basic information about the LakeShore controller.
        -        :return: a multiline string with information about the device
        -        """
        -        raise NotImplementedError
        -
        -    @dynamic_interface
        -    def get_temperature(self, channel) -> float:
        -        """
        -        Retrieve the temperature from the given channel.
        -
        -        Returns:
        -            The current temperature for the given channel.
        -        """
        -        raise NotImplementedError
        -
        -
        -class LakeShore336Simulator(LakeShore336Interface):
        -    """
        -    The LakeShore controller Simulator class for Model 336.
        -    """
        -
        -    def __init__(self):
        -        self._is_connected = True
        -        self._randomwalk = RandomWalk(start=15, boundary=(-100, 25), scale=0.01, count=0)
        -
        -    def is_connected(self):
        -        return self._is_connected
        -
        -    def is_simulator(self):
        -        return True
        -
        -    def connect(self):
        -        self._is_connected = True
        -
        -    def disconnect(self):
        -        self._is_connected = False
        -
        -    def reconnect(self):
        -        if self.is_connected():
        -            self.disconnect()
        -        self.connect()
        -
        -    def info(self):
        -        return "LSCI - The LakeShore Simulator"
        -
        -    def get_temperature(self, channel) -> float:
        -        return next(self._randomwalk)
        -
        -
        -class LakeShore336Controller(LakeShore336Interface):
        -    """
        -    The LakeShore Controller class for Model 336.
        -    """
        -
        -    def __init__(self, hostname=LAKESHORE_SETTINGS.HOSTNAME, port=LAKESHORE_SETTINGS.PORT):
        -        """Initialize the LakeShoreController interface."""
        -
        -        super().__init__()
        -
        -        logger.debug(f"Initializing LakeShoreController with hostname={hostname} on port={port}")
        -
        -        try:
        -            self.ls = LakeShore336EthernetInterface(hostname, port)
        -        except LakeShoreError as exc:
        -            logger.warning(f"LakeShoreError caught: Couldn't establish connection ({exc})")
        -            raise LakeShoreError(
        -                "Couldn't establish a connection with the LakeShore controller."
        -            ) from exc
        -
        -    def connect(self):
        -        try:
        -            self.ls.connect()
        -        except LakeShoreError as exc:
        -            logger.warning(f"LakeShoreError caught: Couldn't establish connection ({exc})")
        -            raise ConnectionError(
        -                "Couldn't establish a connection with the LakeShore Controller."
        -            ) from exc
        -
        -    def disconnect(self):
        -        try:
        -            self.ls.disconnect()
        -        except LakeShoreError as exc:
        -            raise ConnectionError("Couldn't disconnect from LakeShore Controller.") from exc
        -
        -    def reconnect(self):
        -        if self.is_connected():
        -            self.disconnect()
        -        self.connect()
        -
        -    def is_simulator(self):
        -        return False
        -
        -    def is_connected(self):
        -        """Check if the LakeShore Controller is connected. """
        -        return self.ls.is_connected()
        -
        -    def info(self) -> str:
        -        return self.ls.info()
        -
        -    def get_temperature(self, channel: str) -> float:
        -        cmd_string = f"KRDG? {channel}"
        -        logger.debug(f"Requesting temperature from LakeShore Controller: {cmd_string}")
        -        return self._get_response(cmd_string)
        -
        -    def _get_response(self, cmd_string):
        -        response = self.ls.get_response(cmd_string)
        -        return response
        -
        -
        -class LakeShore336Proxy(Proxy, LakeShore336Interface):
        -    """
        -    The LakeShoreProxy class is used to connect to the LakeShore control server and send commands
        -    to the LakeShore Hardware Controller remotely.
        -    """
        -
        -    def __init__(
        -        self,
        -        protocol=CTRL_SETTINGS.PROTOCOL,
        -        hostname=CTRL_SETTINGS.HOSTNAME,
        -        port=CTRL_SETTINGS.COMMANDING_PORT,
        -    ):
        -        """
        -        Args:
        -            protocol: the transport protocol [default is taken from settings file]
        -            hostname: location of the control server (IP address)
        -                [default is taken from settings file]
        -            port: TCP port on which the control server is listening for commands
        -                [default is taken from settings file]
        -        """
        -        super().__init__(connect_address(protocol, hostname, port))
        -
        -
        -
        -
        -
        -
        -
        -
        -
        -

        Classes

        -
        -
        -class LakeShore336Controller -(hostname='localhost', port=7777) -
        -
        -

        The LakeShore Controller class for Model 336.

        -

        Initialize the LakeShoreController interface.

        -
        - -Expand source code - -
        class LakeShore336Controller(LakeShore336Interface):
        -    """
        -    The LakeShore Controller class for Model 336.
        -    """
        -
        -    def __init__(self, hostname=LAKESHORE_SETTINGS.HOSTNAME, port=LAKESHORE_SETTINGS.PORT):
        -        """Initialize the LakeShoreController interface."""
        -
        -        super().__init__()
        -
        -        logger.debug(f"Initializing LakeShoreController with hostname={hostname} on port={port}")
        -
        -        try:
        -            self.ls = LakeShore336EthernetInterface(hostname, port)
        -        except LakeShoreError as exc:
        -            logger.warning(f"LakeShoreError caught: Couldn't establish connection ({exc})")
        -            raise LakeShoreError(
        -                "Couldn't establish a connection with the LakeShore controller."
        -            ) from exc
        -
        -    def connect(self):
        -        try:
        -            self.ls.connect()
        -        except LakeShoreError as exc:
        -            logger.warning(f"LakeShoreError caught: Couldn't establish connection ({exc})")
        -            raise ConnectionError(
        -                "Couldn't establish a connection with the LakeShore Controller."
        -            ) from exc
        -
        -    def disconnect(self):
        -        try:
        -            self.ls.disconnect()
        -        except LakeShoreError as exc:
        -            raise ConnectionError("Couldn't disconnect from LakeShore Controller.") from exc
        -
        -    def reconnect(self):
        -        if self.is_connected():
        -            self.disconnect()
        -        self.connect()
        -
        -    def is_simulator(self):
        -        return False
        -
        -    def is_connected(self):
        -        """Check if the LakeShore Controller is connected. """
        -        return self.ls.is_connected()
        -
        -    def info(self) -> str:
        -        return self.ls.info()
        -
        -    def get_temperature(self, channel: str) -> float:
        -        cmd_string = f"KRDG? {channel}"
        -        logger.debug(f"Requesting temperature from LakeShore Controller: {cmd_string}")
        -        return self._get_response(cmd_string)
        -
        -    def _get_response(self, cmd_string):
        -        response = self.ls.get_response(cmd_string)
        -        return response
        -
        -

        Ancestors

        - -

        Methods

        -
        -
        -def is_connected(self) -
        -
        -

        Check if the LakeShore Controller is connected.

        -
        - -Expand source code - -
        def is_connected(self):
        -    """Check if the LakeShore Controller is connected. """
        -    return self.ls.is_connected()
        -
        -
        -
        -

        Inherited members

        - -
        -
        -class LakeShore336Interface -
        -
        -

        The LakeShore base class.

        -
        - -Expand source code - -
        class LakeShore336Interface(DeviceInterface):
        -    """
        -    The LakeShore base class.
        -    """
        -
        -    @dynamic_interface
        -    def info(self) -> str:
        -        """
        -        Retrieve basic information about the LakeShore controller.
        -        :return: a multiline string with information about the device
        -        """
        -        raise NotImplementedError
        -
        -    @dynamic_interface
        -    def get_temperature(self, channel) -> float:
        -        """
        -        Retrieve the temperature from the given channel.
        -
        -        Returns:
        -            The current temperature for the given channel.
        -        """
        -        raise NotImplementedError
        -
        -

        Ancestors

        - -

        Subclasses

        - -

        Methods

        -
        -
        -def get_temperature(self, channel) ‑> float -
        -
        -

        Retrieve the temperature from the given channel.

        -

        Returns

        -

        The current temperature for the given channel.

        -
        - -Expand source code - -
        @dynamic_interface
        -def get_temperature(self, channel) -> float:
        -    """
        -    Retrieve the temperature from the given channel.
        -
        -    Returns:
        -        The current temperature for the given channel.
        -    """
        -    raise NotImplementedError
        -
        -
        -
        -def info(self) ‑> str -
        -
        -

        Retrieve basic information about the LakeShore controller. -:return: a multiline string with information about the device

        -
        - -Expand source code - -
        @dynamic_interface
        -def info(self) -> str:
        -    """
        -    Retrieve basic information about the LakeShore controller.
        -    :return: a multiline string with information about the device
        -    """
        -    raise NotImplementedError
        -
        -
        -
        -

        Inherited members

        - -
        -
        -class LakeShore336Proxy -(protocol='tcp', hostname='localhost', port=6900) -
        -
        -

        The LakeShoreProxy class is used to connect to the LakeShore control server and send commands -to the LakeShore Hardware Controller remotely.

        -

        Args

        -
        -
        protocol
        -
        the transport protocol [default is taken from settings file]
        -
        hostname
        -
        location of the control server (IP address) -[default is taken from settings file]
        -
        port
        -
        TCP port on which the control server is listening for commands -[default is taken from settings file]
        -
        -
        - -Expand source code - -
        class LakeShore336Proxy(Proxy, LakeShore336Interface):
        -    """
        -    The LakeShoreProxy class is used to connect to the LakeShore control server and send commands
        -    to the LakeShore Hardware Controller remotely.
        -    """
        -
        -    def __init__(
        -        self,
        -        protocol=CTRL_SETTINGS.PROTOCOL,
        -        hostname=CTRL_SETTINGS.HOSTNAME,
        -        port=CTRL_SETTINGS.COMMANDING_PORT,
        -    ):
        -        """
        -        Args:
        -            protocol: the transport protocol [default is taken from settings file]
        -            hostname: location of the control server (IP address)
        -                [default is taken from settings file]
        -            port: TCP port on which the control server is listening for commands
        -                [default is taken from settings file]
        -        """
        -        super().__init__(connect_address(protocol, hostname, port))
        -
        -

        Ancestors

        - -

        Inherited members

        - -
        -
        -class LakeShore336Simulator -
        -
        -

        The LakeShore controller Simulator class for Model 336.

        -
        - -Expand source code - -
        class LakeShore336Simulator(LakeShore336Interface):
        -    """
        -    The LakeShore controller Simulator class for Model 336.
        -    """
        -
        -    def __init__(self):
        -        self._is_connected = True
        -        self._randomwalk = RandomWalk(start=15, boundary=(-100, 25), scale=0.01, count=0)
        -
        -    def is_connected(self):
        -        return self._is_connected
        -
        -    def is_simulator(self):
        -        return True
        -
        -    def connect(self):
        -        self._is_connected = True
        -
        -    def disconnect(self):
        -        self._is_connected = False
        -
        -    def reconnect(self):
        -        if self.is_connected():
        -            self.disconnect()
        -        self.connect()
        -
        -    def info(self):
        -        return "LSCI - The LakeShore Simulator"
        -
        -    def get_temperature(self, channel) -> float:
        -        return next(self._randomwalk)
        -
        -

        Ancestors

        - -

        Inherited members

        - -
        -
        -
        -
        - -
        - - - \ No newline at end of file diff --git a/docs/api/egse/tempcontrol/lakeshore/lsci336_cs.html b/docs/api/egse/tempcontrol/lakeshore/lsci_cs.html similarity index 55% rename from docs/api/egse/tempcontrol/lakeshore/lsci336_cs.html rename to docs/api/egse/tempcontrol/lakeshore/lsci_cs.html index 5e5524e..529a5f2 100644 --- a/docs/api/egse/tempcontrol/lakeshore/lsci336_cs.html +++ b/docs/api/egse/tempcontrol/lakeshore/lsci_cs.html @@ -4,7 +4,7 @@ -egse.tempcontrol.lakeshore.lsci336_cs API documentation +egse.tempcontrol.lakeshore.lsci_cs API documentation @@ -19,7 +19,7 @@
        -

        Module egse.tempcontrol.lakeshore.lsci336_cs

        +

        Module egse.tempcontrol.lakeshore.lsci_cs

        @@ -30,21 +30,45 @@

        Module egse.tempcontrol.lakeshore.lsci336_cs

        import click import logging import sys - +import rich import zmq +import multiprocessing +multiprocessing.current_process().name = "lsci_cs" + from egse.control import ControlServer +from egse.control import is_control_server_active from egse.settings import Settings -from egse.tempcontrol.lakeshore.lsci336 import LakeShore336Proxy -from egse.tempcontrol.lakeshore.lsci336_protocol import LakeShore336Protocol +from egse.tempcontrol.lakeshore.lsci import LakeShoreProxy +from egse.tempcontrol.lakeshore.lsci_protocol import LakeShoreProtocol +from prometheus_client import start_http_server +from egse.zmq_ser import connect_address logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL) -logger = logging.getLogger("LSCI336 CS") +logger = logging.getLogger("LSCI CS") CTRL_SETTINGS = Settings.load("LakeShore Control Server") -class LakeShore336ControlServer(ControlServer): + + +def is_lsci_cs_active(timeout: float = 0.5): + """Check if the LSCI Control Server is running. + + Args: + timeout (float): timeout when waiting for a reply [seconds, default=0.5] + Returns: + True if the control server is running and replied with the expected answer. + """ + + endpoint = connect_address( + CTRL_SETTINGS.PROTOCOL, CTRL_SETTINGS.HOSTNAME, CTRL_SETTINGS.COMMANDING_PORT + ) + + return is_control_server_active(endpoint, timeout) + + +class LakeShoreControlServer(ControlServer): """ LakeShoreControlServer - Command and monitor the LakeShore Temperature Controllers. @@ -60,10 +84,12 @@

        Module egse.tempcontrol.lakeshore.lsci336_cs

        """ - def __init__(self): + def __init__(self, index): + self.index = index + self.name = "LS_"+str(index) + print(index) super().__init__() - - self.device_protocol = LakeShore336Protocol(self) + self.device_protocol = LakeShoreProtocol(self, self.index) logger.debug(f"Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}") @@ -75,20 +101,24 @@

        Module egse.tempcontrol.lakeshore.lsci336_cs

        return CTRL_SETTINGS.PROTOCOL def get_commanding_port(self): - return CTRL_SETTINGS.COMMANDING_PORT + return CTRL_SETTINGS[self.name]['COMMANDING_PORT'] def get_service_port(self): - return CTRL_SETTINGS.SERVICE_PORT + return CTRL_SETTINGS[self.name]['SERVICE_PORT'] def get_monitoring_port(self): - return CTRL_SETTINGS.MONITORING_PORT + return CTRL_SETTINGS[self.name]['MONITORING_PORT'] def get_storage_mnemonic(self): try: - return CTRL_SETTINGS.STORAGE_MNEMONIC + return CTRL_SETTINGS[self.name]['STORAGE_MNEMONIC'] except AttributeError: - return "LSCI336" + return f'LSCI_{self.index}' + def before_serve(self): + start_http_server(CTRL_SETTINGS[self.name]['METRICS_PORT']) + def after_serve(self): + self.device_protocol.synoptics.disconnect_cs() @click.group() def cli(): @@ -96,31 +126,34 @@

        Module egse.tempcontrol.lakeshore.lsci336_cs

        @cli.command() -@click.option("--simulator", "--sim", is_flag=True, help="Start the Lakeshore LSCI336 Simulator as the backend.") -def start(simulator): - """Start the LakeShore LSCI336 Control Server.""" +@click.option("--simulator", "--sim", is_flag=True, help="Start the Lakeshore LSCI Simulator as the backend.") +@click.argument('index', type=click.IntRange(1, 3)) +def start(simulator,index): + """Start the LakeShore LSCI Control Server.""" if simulator: Settings.set_simulation_mode(True) try: + logger.debug(f'Starting LakeShore {index} Control Server') + multiprocessing.current_process().name = f"lsci_cs_{index}" - controller = LakeShore336ControlServer() + controller = LakeShoreControlServer(index) controller.serve() except KeyboardInterrupt: - print("Shutdown requested...exiting") + logger.exception("Shutdown requested...exiting") except SystemExit as exit_code: - print("System Exit with code {}.".format(exit_code)) + logger.exception("System Exit with code {}.".format(exit_code)) sys.exit(exit_code) except Exception: - logger.exception("Cannot start the LakeShore LSCI336 Control Server") + logger.exception("Cannot start the LakeShore LSCI Control Server") # The above line does exactly the same as the traceback, but on the logger # import traceback @@ -130,14 +163,39 @@

        Module egse.tempcontrol.lakeshore.lsci336_cs

        @cli.command() -def stop(): - """Send a 'quit_server' command to the LakeShore LSCI336 Control Server.""" +@click.argument('index', type=click.IntRange(1, 3)) +def stop(index): + """Send a 'quit_server' command to the LakeShore LSCI Control Server.""" - with LakeShore336Proxy() as proxy: + with LakeShoreProxy(index) as proxy: sp = proxy.get_service_proxy() sp.quit_server() +@cli.command() +@click.argument('index', type=click.IntRange(1, 3)) +def status(index): + """Request status information from the Control Server.""" + name = "LS_"+str(index) + protocol = CTRL_SETTINGS.PROTOCOL + hostname = CTRL_SETTINGS.HOSTNAME + port = CTRL_SETTINGS[name]['COMMANDING_PORT'] + + endpoint = connect_address(protocol, hostname, port) + + if is_control_server_active(endpoint): + rich.print(f"LSCI {index} CS: [green]active") + with LakeShoreProxy(index) as lsci: + sim = lsci.is_simulator() + connected = lsci.is_connected() + ip = lsci.get_ip_address() + rich.print(f"mode: {'simulator' if sim else 'device'}{' not' if not connected else ''} connected") + rich.print(f"hostname: {ip}") + else: + rich.print(f"LSCI {index} CS: [red]not active") + + + if __name__ == "__main__": @@ -149,12 +207,48 @@

        Module egse.tempcontrol.lakeshore.lsci336_cs

        +

        Functions

        +
        +
        +def is_lsci_cs_active(timeout: float = 0.5) +
        +
        +

        Check if the LSCI Control Server is running.

        +

        Args

        +
        +
        timeout : float
        +
        timeout when waiting for a reply [seconds, default=0.5]
        +
        +

        Returns

        +

        True if the control server is running and replied with the expected answer.

        +
        + +Expand source code + +
        def is_lsci_cs_active(timeout: float = 0.5):
        +    """Check if the LSCI Control Server is running.
        +
        +    Args:
        +        timeout (float): timeout when waiting for a reply [seconds, default=0.5]
        +    Returns:
        +        True if the control server is running and replied with the expected answer.
        +    """
        +
        +    endpoint = connect_address(
        +        CTRL_SETTINGS.PROTOCOL, CTRL_SETTINGS.HOSTNAME, CTRL_SETTINGS.COMMANDING_PORT
        +    )
        +
        +    return is_control_server_active(endpoint, timeout)
        +
        +
        +

        Classes

        -
        -class LakeShore336ControlServer +
        +class LakeShoreControlServer +(index)

        LakeShoreControlServer - Command and monitor the LakeShore Temperature Controllers.

        @@ -174,7 +268,7 @@

        Classes

        Expand source code -
        class LakeShore336ControlServer(ControlServer):
        +
        class LakeShoreControlServer(ControlServer):
             """
             LakeShoreControlServer - Command and monitor the LakeShore Temperature Controllers.
         
        @@ -190,10 +284,12 @@ 

        Classes

        """ - def __init__(self): + def __init__(self, index): + self.index = index + self.name = "LS_"+str(index) + print(index) super().__init__() - - self.device_protocol = LakeShore336Protocol(self) + self.device_protocol = LakeShoreProtocol(self, self.index) logger.debug(f"Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}") @@ -205,19 +301,24 @@

        Classes

        return CTRL_SETTINGS.PROTOCOL def get_commanding_port(self): - return CTRL_SETTINGS.COMMANDING_PORT + return CTRL_SETTINGS[self.name]['COMMANDING_PORT'] def get_service_port(self): - return CTRL_SETTINGS.SERVICE_PORT + return CTRL_SETTINGS[self.name]['SERVICE_PORT'] def get_monitoring_port(self): - return CTRL_SETTINGS.MONITORING_PORT + return CTRL_SETTINGS[self.name]['MONITORING_PORT'] def get_storage_mnemonic(self): try: - return CTRL_SETTINGS.STORAGE_MNEMONIC + return CTRL_SETTINGS[self.name]['STORAGE_MNEMONIC'] except AttributeError: - return "LSCI336"
        + return f'LSCI_{self.index}' + def before_serve(self): + start_http_server(CTRL_SETTINGS[self.name]['METRICS_PORT']) + + def after_serve(self): + self.device_protocol.synoptics.disconnect_cs()

        Ancestors

          @@ -225,7 +326,33 @@

          Ancestors

        Methods

        -
        +
        +def after_serve(self) +
        +
        +
        +
        + +Expand source code + +
        def after_serve(self):
        +    self.device_protocol.synoptics.disconnect_cs()
        +
        +
        +
        +def before_serve(self) +
        +
        +
        +
        + +Expand source code + +
        def before_serve(self):
        +    start_http_server(CTRL_SETTINGS[self.name]['METRICS_PORT'])
        +
        +
        +
        def get_commanding_port(self)
        @@ -235,10 +362,10 @@

        Methods

        Expand source code
        def get_commanding_port(self):
        -    return CTRL_SETTINGS.COMMANDING_PORT
        + return CTRL_SETTINGS[self.name]['COMMANDING_PORT']
        -
        +
        def get_communication_protocol(self)
        @@ -251,7 +378,7 @@

        Methods

        return CTRL_SETTINGS.PROTOCOL
        -
        +
        def get_monitoring_port(self)
        @@ -261,10 +388,10 @@

        Methods

        Expand source code
        def get_monitoring_port(self):
        -    return CTRL_SETTINGS.MONITORING_PORT
        + return CTRL_SETTINGS[self.name]['MONITORING_PORT']
        -
        +
        def get_service_port(self)
        @@ -274,10 +401,10 @@

        Methods

        Expand source code
        def get_service_port(self):
        -    return CTRL_SETTINGS.SERVICE_PORT
        + return CTRL_SETTINGS[self.name]['SERVICE_PORT']
        -
        +
        def get_storage_mnemonic(self)
        @@ -288,9 +415,9 @@

        Methods

        def get_storage_mnemonic(self):
             try:
        -        return CTRL_SETTINGS.STORAGE_MNEMONIC
        +        return CTRL_SETTINGS[self.name]['STORAGE_MNEMONIC']
             except AttributeError:
        -        return "LSCI336"
        + return f'LSCI_{self.index}'
        @@ -298,10 +425,15 @@

        Inherited members

        +
      • Functions

        + +
      • Classes

        diff --git a/docs/api/egse/tempcontrol/lakeshore/lsci336_devif.html b/docs/api/egse/tempcontrol/lakeshore/lsci_devif.html similarity index 58% rename from docs/api/egse/tempcontrol/lakeshore/lsci336_devif.html rename to docs/api/egse/tempcontrol/lakeshore/lsci_devif.html index 2824c37..5361c5c 100644 --- a/docs/api/egse/tempcontrol/lakeshore/lsci336_devif.html +++ b/docs/api/egse/tempcontrol/lakeshore/lsci_devif.html @@ -4,7 +4,7 @@ -egse.tempcontrol.lakeshore.lsci336_devif API documentation +egse.tempcontrol.lakeshore.lsci_devif API documentation @@ -19,7 +19,7 @@
        -

        Module egse.tempcontrol.lakeshore.lsci336_devif

        +

        Module egse.tempcontrol.lakeshore.lsci_devif

        @@ -33,11 +33,10 @@

        Module egse.tempcontrol.lakeshore.lsci336_devifModule egse.tempcontrol.lakeshore.lsci336_devifModule egse.tempcontrol.lakeshore.lsci336_devifModule egse.tempcontrol.lakeshore.lsci336_devifModule egse.tempcontrol.lakeshore.lsci336_devifModule egse.tempcontrol.lakeshore.lsci336_devifModule egse.tempcontrol.lakeshore.lsci336_devifModule egse.tempcontrol.lakeshore.lsci336_devif + return self.get_response("*IDN?\n") + + def get_id(self): + return self.name + + def get_temperature(self): + return self.get_response("CRDG? A\n") + + def get_params_pid(self, output:int): + return self.get_response(f"PID? {output}\n").split(",") + + def get_setpoint(self, output:int): + return self.get_response(f"SETP? {output}\n",) + + def get_tuning_status(self): + return self.get_response(f"TUNEST?\n").split(",") + + def get_heater_setup(self, output:int): + return self.get_response(f"HTRSET? {output}\n").split(",") + + def get_range(self, output:int): + return self.write(f"RANGE? {output}\n") + + def get_heater_status(self, output:int): + return self.get_response(f"HTRST? {output}\n") + + def get_heater(self, output:int): + return self.get_response(f"HTR? {output}\n",) + + def set_params_pid(self, output:int, p:int, i:int, d:int): + self.write(f"PID {output} {p} {i} {d}\n") + + def set_setpoint(self, outpout:int, value:str): + self.write(f"SETP {outpout},{value}\n") + + def set_autotune(self, output:int, mode:int): + self.write(f"ATUNE {output},{mode}\n") + + def set_heater_setup(self,output:int, heater_resistant:int, max_current:int, max_user_current:str, output_display:int): + self.write(f"HTRSET {output},{heater_resistant},{max_current},{max_user_current},{output_display}\n") + + def set_range(self, output:int, range:int): + self.write(f"RANGE {output},{range}\n")

        @@ -259,8 +329,8 @@

        Module egse.tempcontrol.lakeshore.lsci336_devif

        Classes

        -
        -class LakeShore336Command +
        +class LakeShoreCommand (name, cmd, response=None, wait=None, check=None, description=None, device_method=None)
        @@ -274,7 +344,7 @@

        Classes

        Expand source code -
        class LakeShore336Command(ClientServerCommand):
        +
        class LakeShoreCommand(ClientServerCommand):
             def get_cmd_string(self, *args, **kwargs) -> str:
                 out = super().get_cmd_string(*args, **kwargs)
                 return out + "\n"
        @@ -286,7 +356,7 @@

        Ancestors

      Methods

      -
      +
      def get_cmd_string(self, *args, **kwargs) ‑> str
      @@ -311,9 +381,30 @@

      Inherited members

    -
    -class LakeShore336EthernetInterface -(hostname=None, port=None) +
    +class LakeShoreError +(*args, **kwargs) +
    +
    +

    Base exception for all LakeShore errors.

    +
    + +Expand source code + +
    class LakeShoreError(Exception):
    +    """Base exception for all LakeShore errors."""
    +
    +    pass
    +
    +

    Ancestors

    +
      +
    • builtins.Exception
    • +
    • builtins.BaseException
    • +
    +
    +
    +class LakeShoreEthernetInterface +(device_index)
    @@ -321,10 +412,11 @@

    Inherited members

    Expand source code -
    class LakeShore336EthernetInterface:
    -    def __init__(self, hostname=None, port=None):
    -        self.hostname = LS_SETTINGS.HOSTNAME if hostname is None else hostname
    -        self.port = LS_SETTINGS.PORT if port is None else port
    +
    class LakeShoreEthernetInterface:
    +    def __init__(self, device_index):
    +        self.name = "LS_"+str(device_index)
    +        self.hostname = LS_SETTINGS[self.name]['HOSTNAME']
    +        self.port = LS_SETTINGS[self.name]['PORT']
             self.sock = None
     
             # Access-to-the-connection semaphore.
    @@ -334,6 +426,7 @@ 

    Inherited members

    self.semaphore = threading.Semaphore() self.isConnectionOpen = False + self.connect() def connect(self): @@ -417,7 +510,7 @@

    Inherited members

    return False try: - response = self.get_response(IDENTIFICATION_QUERY) + response = self.get_response("*IDN?\n") except LakeShoreError as e_lakeshore: if len(e_lakeshore.args) >= 2 and e_lakeshore.args[1] == -1: MODULE_LOGGER.error( @@ -453,7 +546,7 @@

    Inherited members

    return True - def get_response(self, command: str, wait=True): + def get_response(self, command: str, wait=True)-> str: """ Send a single command to the controller and block until a response from the controller. @@ -483,8 +576,8 @@

    Inherited members

    return_string = self.wait_for_response() - return return_string.decode() - + return return_string.decode().replace("\r\n", "").replace("+","") + except socket.timeout as e_timeout: raise LakeShoreError("Socket timeout error") from e_timeout except socket.error as e_socket: @@ -494,13 +587,41 @@

    Inherited members

    if wait: self.semaphore.release() + def write(self, command: str): + """ + Send a single command to the device controller without waiting for a response. + + Args: + command: an order command for the controller. + + Raises: + DeviceConnectionError when the command could not be sent due to a + communication problem. + + DeviceTimeoutError when the command could not be sent due to a timeout. + """ + + try: + self.sock.sendall(command.encode()) + + except socket.timeout as e_timeout: + raise LakeShoreError("Socket timeout error") from e_timeout + except socket.error as e_socket: + # Interpret any socket-related error as a connection error + raise LakeShoreError("Socket communication error.") from e_socket + except AttributeError: + if not self.is_connected: + msg = "The LakeShore is not connected, use the connect() method." + raise LakeShoreError(msg) + raise + def wait_for_response(self): n_total = 0 buf_size = 2048 try: for idx in range(100): - time.sleep(0.001) # Give the device time to fill the buffer + time.sleep(0.05) # Give the device time to fill the buffer data = self.sock.recv(buf_size) n = len(data) n_total += n @@ -510,16 +631,58 @@

    Inherited members

    MODULE_LOGGER.warning(f"Socket timeout error from {e_timeout}") return b"\r\n" - MODULE_LOGGER.debug(f"Total number of bytes received is {n_total}, idx={idx}") + # MODULE_LOGGER.debug(f"Total number of bytes received is {n_total}, idx={idx}") return data def info(self): - return self.get_response(IDENTIFICATION_QUERY)
    + return self.get_response("*IDN?\n") + + def get_id(self): + return self.name + + def get_temperature(self): + return self.get_response("CRDG? A\n") + + def get_params_pid(self, output:int): + return self.get_response(f"PID? {output}\n").split(",") + + def get_setpoint(self, output:int): + return self.get_response(f"SETP? {output}\n",) + + def get_tuning_status(self): + return self.get_response(f"TUNEST?\n").split(",") + + def get_heater_setup(self, output:int): + return self.get_response(f"HTRSET? {output}\n").split(",") + + def get_range(self, output:int): + return self.write(f"RANGE? {output}\n") + + def get_heater_status(self, output:int): + return self.get_response(f"HTRST? {output}\n") + + def get_heater(self, output:int): + return self.get_response(f"HTR? {output}\n",) + + def set_params_pid(self, output:int, p:int, i:int, d:int): + self.write(f"PID {output} {p} {i} {d}\n") + + def set_setpoint(self, outpout:int, value:str): + self.write(f"SETP {outpout},{value}\n") + + def set_autotune(self, output:int, mode:int): + self.write(f"ATUNE {output},{mode}\n") + + def set_heater_setup(self,output:int, heater_resistant:int, max_current:int, max_user_current:str, output_display:int): + self.write(f"HTRSET {output},{heater_resistant},{max_current},{max_user_current},{output_display}\n") + + def set_range(self, output:int, range:int): + self.write(f"RANGE {output},{range}\n")

    Methods

    -
    +
    def connect(self)
    @@ -589,7 +752,7 @@

    Methods

    raise LakeShoreError("Device is not connected, check logging messages for the cause.")
    -
    +
    def disconnect(self)
    @@ -616,8 +779,86 @@

    Methods

    raise LakeShoreError(f"Could not close socket to {self.hostname}") from e_exc
    -
    -def get_response(self, command: str, wait=True) +
    +def get_heater(self, output: int) +
    +
    +
    +
    + +Expand source code + +
    def get_heater(self, output:int):
    +    return self.get_response(f"HTR? {output}\n",)
    +
    +
    +
    +def get_heater_setup(self, output: int) +
    +
    +
    +
    + +Expand source code + +
    def get_heater_setup(self, output:int):
    +    return self.get_response(f"HTRSET? {output}\n").split(",")
    +
    +
    +
    +def get_heater_status(self, output: int) +
    +
    +
    +
    + +Expand source code + +
    def get_heater_status(self, output:int):
    +    return self.get_response(f"HTRST? {output}\n")
    +
    +
    +
    +def get_id(self) +
    +
    +
    +
    + +Expand source code + +
    def get_id(self):
    +    return self.name
    +
    +
    +
    +def get_params_pid(self, output: int) +
    +
    +
    +
    + +Expand source code + +
    def get_params_pid(self, output:int):
    +    return self.get_response(f"PID? {output}\n").split(",")
    +
    +
    +
    +def get_range(self, output: int) +
    +
    +
    +
    + +Expand source code + +
    def get_range(self, output:int):
    +    return self.write(f"RANGE? {output}\n")
    +
    +
    +
    +def get_response(self, command: str, wait=True) ‑> str

    Send a single command to the controller and block until a response from the controller.

    @@ -634,7 +875,7 @@

    Methods

    Expand source code -
    def get_response(self, command: str, wait=True):
    +
    def get_response(self, command: str, wait=True)-> str:
         """
         Send a single command to the controller and block until a response from the controller.
     
    @@ -664,8 +905,8 @@ 

    Methods

    return_string = self.wait_for_response() - return return_string.decode() - + return return_string.decode().replace("\r\n", "").replace("+","") + except socket.timeout as e_timeout: raise LakeShoreError("Socket timeout error") from e_timeout except socket.error as e_socket: @@ -676,7 +917,46 @@

    Methods

    self.semaphore.release()
    -
    +
    +def get_setpoint(self, output: int) +
    +
    +
    +
    + +Expand source code + +
    def get_setpoint(self, output:int):
    +    return self.get_response(f"SETP? {output}\n",)
    +
    +
    +
    +def get_temperature(self) +
    +
    +
    +
    + +Expand source code + +
    def get_temperature(self):
    +    return self.get_response("CRDG? A\n")
    +
    +
    +
    +def get_tuning_status(self) +
    +
    +
    +
    + +Expand source code + +
    def get_tuning_status(self):
    +    return self.get_response(f"TUNEST?\n").split(",")
    +
    +
    +
    def info(self)
    @@ -686,10 +966,10 @@

    Methods

    Expand source code
    def info(self):
    -    return self.get_response(IDENTIFICATION_QUERY)
    + return self.get_response("*IDN?\n")
    -
    +
    def is_connected(self)
    @@ -704,7 +984,7 @@

    Methods

    return False try: - response = self.get_response(IDENTIFICATION_QUERY) + response = self.get_response("*IDN?\n") except LakeShoreError as e_lakeshore: if len(e_lakeshore.args) >= 2 and e_lakeshore.args[1] == -1: MODULE_LOGGER.error( @@ -741,7 +1021,72 @@

    Methods

    return True
    -
    +
    +def set_autotune(self, output: int, mode: int) +
    +
    +
    +
    + +Expand source code + +
    def set_autotune(self, output:int, mode:int):
    +    self.write(f"ATUNE {output},{mode}\n")
    +
    +
    +
    +def set_heater_setup(self, output: int, heater_resistant: int, max_current: int, max_user_current: str, output_display: int) +
    +
    +
    +
    + +Expand source code + +
    def set_heater_setup(self,output:int, heater_resistant:int, max_current:int, max_user_current:str, output_display:int):
    +    self.write(f"HTRSET {output},{heater_resistant},{max_current},{max_user_current},{output_display}\n")
    +
    +
    +
    +def set_params_pid(self, output: int, p: int, i: int, d: int) +
    +
    +
    +
    + +Expand source code + +
    def set_params_pid(self, output:int, p:int, i:int, d:int):
    +    self.write(f"PID {output} {p} {i} {d}\n")
    +
    +
    +
    +def set_range(self, output: int, range: int) +
    +
    +
    +
    + +Expand source code + +
    def set_range(self, output:int, range:int):
    +    self.write(f"RANGE {output},{range}\n")
    +
    +
    +
    +def set_setpoint(self, outpout: int, value: str) +
    +
    +
    +
    + +Expand source code + +
    def set_setpoint(self, outpout:int, value:str):
    +    self.write(f"SETP {outpout},{value}\n")
    +
    +
    +
    def wait_for_response(self)
    @@ -756,7 +1101,7 @@

    Methods

    try: for idx in range(100): - time.sleep(0.001) # Give the device time to fill the buffer + time.sleep(0.05) # Give the device time to fill the buffer data = self.sock.recv(buf_size) n = len(data) n_total += n @@ -766,33 +1111,59 @@

    Methods

    MODULE_LOGGER.warning(f"Socket timeout error from {e_timeout}") return b"\r\n" - MODULE_LOGGER.debug(f"Total number of bytes received is {n_total}, idx={idx}") + # MODULE_LOGGER.debug(f"Total number of bytes received is {n_total}, idx={idx}") return data
    -
    -
    -
    -class LakeShoreError -(*args, **kwargs) +
    +def write(self, command: str)
    -

    Base exception for all LakeShore errors.

    +

    Send a single command to the device controller without waiting for a response.

    +

    Args

    +
    +
    command
    +
    an order command for the controller.
    +
    +

    Raises

    +

    DeviceConnectionError when the command could not be sent due to a +communication problem.

    +

    DeviceTimeoutError when the command could not be sent due to a timeout.

    Expand source code -
    class LakeShoreError(Exception):
    -    """Base exception for all LakeShore errors."""
    +
    def write(self, command: str):
    +    """
    +    Send a single command to the device controller without waiting for a response.
     
    -    pass
    + Args: + command: an order command for the controller. + + Raises: + DeviceConnectionError when the command could not be sent due to a + communication problem. + + DeviceTimeoutError when the command could not be sent due to a timeout. + """ + + try: + self.sock.sendall(command.encode()) + + except socket.timeout as e_timeout: + raise LakeShoreError("Socket timeout error") from e_timeout + except socket.error as e_socket: + # Interpret any socket-related error as a connection error + raise LakeShoreError("Socket communication error.") from e_socket + except AttributeError: + if not self.is_connected: + msg = "The LakeShore is not connected, use the connect() method." + raise LakeShoreError(msg) + raise
    -

    Ancestors

    -
      -
    • builtins.Exception
    • -
    • builtins.BaseException
    • -
    +
    +
    @@ -811,24 +1182,39 @@

    Index

  • Classes

  • diff --git a/docs/api/egse/tempcontrol/spid/spid_protocol.html b/docs/api/egse/tempcontrol/lakeshore/lsci_protocol.html similarity index 52% rename from docs/api/egse/tempcontrol/spid/spid_protocol.html rename to docs/api/egse/tempcontrol/lakeshore/lsci_protocol.html index be9fff6..4a42431 100644 --- a/docs/api/egse/tempcontrol/spid/spid_protocol.html +++ b/docs/api/egse/tempcontrol/lakeshore/lsci_protocol.html @@ -4,7 +4,7 @@ -egse.tempcontrol.spid.spid_protocol API documentation +egse.tempcontrol.lakeshore.lsci_protocol API documentation @@ -19,120 +19,89 @@
    -

    Module egse.tempcontrol.spid.spid_protocol

    +

    Module egse.tempcontrol.lakeshore.lsci_protocol

    Expand source code -
    from egse.control import ControlServer
    +
    import logging
    +
    +from egse.control import ControlServer
    +from egse.metrics import define_metrics
     from egse.protocol import CommandProtocol
     from egse.settings import Settings
    -from datetime import datetime
    -from prometheus_client import Gauge
    +from egse.setup import load_setup
     
    -from egse.tempcontrol.spid.spid_controller import PidController
    -from egse.tempcontrol.spid.spid import PidInterface, PidSimulator, PidCommand
     from egse.system import format_datetime
     from egse.zmq_ser import bind_address
    -from egse.confman import ConfigurationManagerProxy
     
    +from egse.tempcontrol.lakeshore.lsci import (LakeShoreController, LakeShoreInterface, LakeShoreSimulator)
    +from egse.tempcontrol.lakeshore.lsci_devif import LakeShoreCommand
     
    -COMMAND_SETTINGS = Settings.load(filename='spid.yaml')
    +from egse.synoptics import SynopticsManagerProxy
     
    +logger = logging.getLogger(__name__)
     
    -class PidProtocol(CommandProtocol):
    +COMMAND_SETTINGS = Settings.load(filename="lsci.yaml")
    +SITE_ID = Settings.load("SITE").ID
     
    -    def __init__(self, control_server:ControlServer):
     
    +class LakeShoreProtocol(CommandProtocol):
    +    def __init__(self, control_server: ControlServer, device_index):
             super().__init__()
             self.control_server = control_server
    -
    +        self.device_index = device_index
             if Settings.simulation_mode():
    -            self.pid = PidSimulator()
    +            self.lakeshore = LakeShoreSimulator()
             else:
    -            self.pid = PidController()
    -
    -        self.load_commands(COMMAND_SETTINGS.Commands, PidCommand, PidInterface)
    -
    -        self.build_device_method_lookup_table(self.pid)
    -
    -        # Get PID channels form the setup
    -        with ConfigurationManagerProxy() as cm:
    -            heaters = cm.get_setup().gse.spid.configuration.heaters
    +            self.lakeshore = LakeShoreController(device_index)
     
    -        # Get the list of PID channe    ls [pid_idx, agilent_idx, agilent_ch, bbb_idx, bbb_ch, P, I]
    -        self.channels = [channel for heater in heaters.values() for channel in heater]
    -    
    +        setup = load_setup()
     
    -        self.gauges_enabled = [Gauge(f'GSRON_PID_CH{channel[0]}_ENABLED', '') for channel in self.channels]
    -        self.gauges_setpoint = [Gauge(f'GSRON_PID_CH{channel[0]}_SETPOINT', '') for channel in self.channels]
    -        self.gauges_input = [Gauge(f'GSRON_PID_CH{channel[0]}_INPUT', '') for channel in self.channels]
    -        self.gauges_error = [Gauge(f'GSRON_PID_CH{channel[0]}_ERROR', '') for channel in self.channels]
    -        self.gauges_output = [Gauge(f'GSRON_PID_CH{channel[0]}_OUTPUT', '') for channel in self.channels]
    -        self.gauges_isum = [Gauge(f'GSRON_PID_CH{channel[0]}_ISUM', '') for channel in self.channels]
    +        self.load_commands(COMMAND_SETTINGS.Commands, LakeShoreCommand, LakeShoreInterface)
     
    +        self.build_device_method_lookup_table(self.lakeshore)
    +        
    +        self.synoptics = SynopticsManagerProxy()
    +        
    +        self.metrics = define_metrics(origin="LSCI", use_site=True, setup=setup)
     
         def get_bind_address(self):
    -
    -        return bind_address(self.control_server.get_communication_protocol(),
    -                            self.control_server.get_commanding_port())
    -
    +        return bind_address(
    +            self.control_server.get_communication_protocol(),
    +            self.control_server.get_commanding_port(),
    +        )
     
         def get_status(self):
    -
    -        status_info = super().get_status()
    -        now = datetime.now()
    -        enabled     = {f"{ch[3]}_{ch[4]}" : self.pid.running[ch[0]] for ch in self.channels}
    -        setpoint    = [self.pid.setpoints[ch[0]] for ch in self.channels]
    -        temperature = [self.pid.temperature[ch[0]] for ch in self.channels]
    -        timestamp   = [self.pid.timestamp[ch[0]] for ch in self.channels]
    -        error       = [self.pid.errors[ch[0]] for ch in self.channels]
    -        isum        = [self.pid.isum[ch[0]] for ch in self.channels]
    -        t_input      = [self.pid.inputs[ch[0]] for ch in self.channels]
    -        output      = [self.pid.outputs[ch[0]] for ch in self.channels]
    -        pid_const   = [[self.pid.pids[ch[0]].Kp, self.pid.pids[ch[0]].Ki, self.pid.pids[ch[0]].Kd] for ch in self.channels]
    -
    -        
    -        status_info['Enabled']      = enabled
    -        status_info['Setpoint']     = setpoint
    -        status_info['Temperature']  = temperature
    -        status_info['Timestamp']    = timestamp
    -        status_info['Error']        = error
    -        status_info['Isum']         = isum
    -        status_info['Input']        = t_input
    -        status_info['Output']       = output
    -        status_info['PidConst']     = pid_const
    -        
    -        return status_info
    -
    +        return super().get_status()
     
         def get_housekeeping(self) -> dict:
    -
    -        hk_dict = {'timestamp': format_datetime()}
    -
    -        for channel in self.channels:
    -
    -            hk_dict[f'GSRON_PID_CH{channel[0]}_ENABLED'] = int(self.pid.running[channel[0]])
    -            hk_dict[f'GSRON_PID_CH{channel[0]}_SETPOINT'] = self.pid.setpoints[channel[0]]
    -            hk_dict[f'GSRON_PID_CH{channel[0]}_INPUT'] = self.pid.inputs[channel[0]]
    -            hk_dict[f'GSRON_PID_CH{channel[0]}_ERROR'] = self.pid.errors[channel[0]]
    -            hk_dict[f'GSRON_PID_CH{channel[0]}_OUTPUT'] = self.pid.outputs[channel[0]]
    -            hk_dict[f'GSRON_PID_CH{channel[0]}_ISUM'] = self.pid.pids[channel[0]].integral
    -
    -            self.gauges_enabled[channel[0]].set(hk_dict[f'GSRON_PID_CH{channel[0]}_ENABLED'])
    -            self.gauges_setpoint[channel[0]].set(hk_dict[f'GSRON_PID_CH{channel[0]}_SETPOINT'])
    -            self.gauges_input[channel[0]].set(hk_dict[f'GSRON_PID_CH{channel[0]}_INPUT'])
    -            self.gauges_error[channel[0]].set(hk_dict[f'GSRON_PID_CH{channel[0]}_ERROR'])
    -            self.gauges_output[channel[0]].set(hk_dict[f'GSRON_PID_CH{channel[0]}_OUTPUT'])
    -            self.gauges_isum[channel[0]].set(hk_dict[f'GSRON_PID_CH{channel[0]}_ISUM'])
    -
    -        return hk_dict
    -    
    -    def quit(self):
    -        self.pid.disconnect()
    -        super().quit()
    + metrics_dict = self.metrics + hk_dict = dict() + hk_dict["timestamp"] = format_datetime() + try: + hk_dict[f"G{SITE_ID}_LSCI_{self.device_index}_TEMP_A"] = self.lakeshore.get_temperature() + pidParams = self.lakeshore.get_params_pid(1) + hk_dict[f"G{SITE_ID}_LSCI_{self.device_index}_P_VALUE"] = pidParams[0] + hk_dict[f"G{SITE_ID}_LSCI_{self.device_index}_I_VALUE"] = pidParams[1] + hk_dict[f"G{SITE_ID}_LSCI_{self.device_index}_D_VALUE"] = pidParams[2] + hk_dict[f"G{SITE_ID}_LSCI_{self.device_index}_HEATER_VALUE"] = self.lakeshore.get_heater(1) + hk_dict[f"G{SITE_ID}_LSCI_{self.device_index}_SET_POINT_VALUE"] = self.lakeshore.get_setpoint(1) + + for hk_name in metrics_dict.keys(): + index_lsci = hk_name.split("_") + if(len(index_lsci) > 2): + if int(index_lsci[2]) == int(self.device_index): + metrics_dict[hk_name].set(hk_dict[hk_name]) + + # Send the HK acquired so far to the Synoptics Manager + self.synoptics.store_th_synoptics(hk_dict) + + except Exception as exc: + logger.warning(f'failed to get HK ({exc})') + return hk_dict
    @@ -144,9 +113,9 @@

    Module egse.tempcontrol.spid.spid_protocol

    Classes

    -
    -class PidProtocol -(control_server: ControlServer) +
    +class LakeShoreProtocol +(control_server: ControlServer, device_index)

    This class is the glue between the control servers and the hardware @@ -161,97 +130,60 @@

    Classes

    Expand source code -
    class PidProtocol(CommandProtocol):
    -
    -    def __init__(self, control_server:ControlServer):
    -
    +
    class LakeShoreProtocol(CommandProtocol):
    +    def __init__(self, control_server: ControlServer, device_index):
             super().__init__()
             self.control_server = control_server
    -
    +        self.device_index = device_index
             if Settings.simulation_mode():
    -            self.pid = PidSimulator()
    +            self.lakeshore = LakeShoreSimulator()
             else:
    -            self.pid = PidController()
    -
    -        self.load_commands(COMMAND_SETTINGS.Commands, PidCommand, PidInterface)
    -
    -        self.build_device_method_lookup_table(self.pid)
    -
    -        # Get PID channels form the setup
    -        with ConfigurationManagerProxy() as cm:
    -            heaters = cm.get_setup().gse.spid.configuration.heaters
    +            self.lakeshore = LakeShoreController(device_index)
     
    -        # Get the list of PID channe    ls [pid_idx, agilent_idx, agilent_ch, bbb_idx, bbb_ch, P, I]
    -        self.channels = [channel for heater in heaters.values() for channel in heater]
    -    
    +        setup = load_setup()
     
    -        self.gauges_enabled = [Gauge(f'GSRON_PID_CH{channel[0]}_ENABLED', '') for channel in self.channels]
    -        self.gauges_setpoint = [Gauge(f'GSRON_PID_CH{channel[0]}_SETPOINT', '') for channel in self.channels]
    -        self.gauges_input = [Gauge(f'GSRON_PID_CH{channel[0]}_INPUT', '') for channel in self.channels]
    -        self.gauges_error = [Gauge(f'GSRON_PID_CH{channel[0]}_ERROR', '') for channel in self.channels]
    -        self.gauges_output = [Gauge(f'GSRON_PID_CH{channel[0]}_OUTPUT', '') for channel in self.channels]
    -        self.gauges_isum = [Gauge(f'GSRON_PID_CH{channel[0]}_ISUM', '') for channel in self.channels]
    +        self.load_commands(COMMAND_SETTINGS.Commands, LakeShoreCommand, LakeShoreInterface)
     
    +        self.build_device_method_lookup_table(self.lakeshore)
    +        
    +        self.synoptics = SynopticsManagerProxy()
    +        
    +        self.metrics = define_metrics(origin="LSCI", use_site=True, setup=setup)
     
         def get_bind_address(self):
    -
    -        return bind_address(self.control_server.get_communication_protocol(),
    -                            self.control_server.get_commanding_port())
    -
    +        return bind_address(
    +            self.control_server.get_communication_protocol(),
    +            self.control_server.get_commanding_port(),
    +        )
     
         def get_status(self):
    -
    -        status_info = super().get_status()
    -        now = datetime.now()
    -        enabled     = {f"{ch[3]}_{ch[4]}" : self.pid.running[ch[0]] for ch in self.channels}
    -        setpoint    = [self.pid.setpoints[ch[0]] for ch in self.channels]
    -        temperature = [self.pid.temperature[ch[0]] for ch in self.channels]
    -        timestamp   = [self.pid.timestamp[ch[0]] for ch in self.channels]
    -        error       = [self.pid.errors[ch[0]] for ch in self.channels]
    -        isum        = [self.pid.isum[ch[0]] for ch in self.channels]
    -        t_input      = [self.pid.inputs[ch[0]] for ch in self.channels]
    -        output      = [self.pid.outputs[ch[0]] for ch in self.channels]
    -        pid_const   = [[self.pid.pids[ch[0]].Kp, self.pid.pids[ch[0]].Ki, self.pid.pids[ch[0]].Kd] for ch in self.channels]
    -
    -        
    -        status_info['Enabled']      = enabled
    -        status_info['Setpoint']     = setpoint
    -        status_info['Temperature']  = temperature
    -        status_info['Timestamp']    = timestamp
    -        status_info['Error']        = error
    -        status_info['Isum']         = isum
    -        status_info['Input']        = t_input
    -        status_info['Output']       = output
    -        status_info['PidConst']     = pid_const
    -        
    -        return status_info
    -
    +        return super().get_status()
     
         def get_housekeeping(self) -> dict:
    -
    -        hk_dict = {'timestamp': format_datetime()}
    -
    -        for channel in self.channels:
    -
    -            hk_dict[f'GSRON_PID_CH{channel[0]}_ENABLED'] = int(self.pid.running[channel[0]])
    -            hk_dict[f'GSRON_PID_CH{channel[0]}_SETPOINT'] = self.pid.setpoints[channel[0]]
    -            hk_dict[f'GSRON_PID_CH{channel[0]}_INPUT'] = self.pid.inputs[channel[0]]
    -            hk_dict[f'GSRON_PID_CH{channel[0]}_ERROR'] = self.pid.errors[channel[0]]
    -            hk_dict[f'GSRON_PID_CH{channel[0]}_OUTPUT'] = self.pid.outputs[channel[0]]
    -            hk_dict[f'GSRON_PID_CH{channel[0]}_ISUM'] = self.pid.pids[channel[0]].integral
    -
    -            self.gauges_enabled[channel[0]].set(hk_dict[f'GSRON_PID_CH{channel[0]}_ENABLED'])
    -            self.gauges_setpoint[channel[0]].set(hk_dict[f'GSRON_PID_CH{channel[0]}_SETPOINT'])
    -            self.gauges_input[channel[0]].set(hk_dict[f'GSRON_PID_CH{channel[0]}_INPUT'])
    -            self.gauges_error[channel[0]].set(hk_dict[f'GSRON_PID_CH{channel[0]}_ERROR'])
    -            self.gauges_output[channel[0]].set(hk_dict[f'GSRON_PID_CH{channel[0]}_OUTPUT'])
    -            self.gauges_isum[channel[0]].set(hk_dict[f'GSRON_PID_CH{channel[0]}_ISUM'])
    -
    -        return hk_dict
    -    
    -    def quit(self):
    -        self.pid.disconnect()
    -        super().quit()
    + metrics_dict = self.metrics + hk_dict = dict() + hk_dict["timestamp"] = format_datetime() + try: + hk_dict[f"G{SITE_ID}_LSCI_{self.device_index}_TEMP_A"] = self.lakeshore.get_temperature() + pidParams = self.lakeshore.get_params_pid(1) + hk_dict[f"G{SITE_ID}_LSCI_{self.device_index}_P_VALUE"] = pidParams[0] + hk_dict[f"G{SITE_ID}_LSCI_{self.device_index}_I_VALUE"] = pidParams[1] + hk_dict[f"G{SITE_ID}_LSCI_{self.device_index}_D_VALUE"] = pidParams[2] + hk_dict[f"G{SITE_ID}_LSCI_{self.device_index}_HEATER_VALUE"] = self.lakeshore.get_heater(1) + hk_dict[f"G{SITE_ID}_LSCI_{self.device_index}_SET_POINT_VALUE"] = self.lakeshore.get_setpoint(1) + + for hk_name in metrics_dict.keys(): + index_lsci = hk_name.split("_") + if(len(index_lsci) > 2): + if int(index_lsci[2]) == int(self.device_index): + metrics_dict[hk_name].set(hk_dict[hk_name]) + + # Send the HK acquired so far to the Synoptics Manager + self.synoptics.store_th_synoptics(hk_dict) + + except Exception as exc: + logger.warning(f'failed to get HK ({exc})') + return hk_dict

    Ancestors

      @@ -291,13 +223,13 @@

      Index

      • Super-module

      • Classes

      • diff --git a/docs/api/egse/tempcontrol/lakeshore/lsci336_ui.html b/docs/api/egse/tempcontrol/lakeshore/lsci_ui.html similarity index 75% rename from docs/api/egse/tempcontrol/lakeshore/lsci336_ui.html rename to docs/api/egse/tempcontrol/lakeshore/lsci_ui.html index ba9b59e..0933cc2 100644 --- a/docs/api/egse/tempcontrol/lakeshore/lsci336_ui.html +++ b/docs/api/egse/tempcontrol/lakeshore/lsci_ui.html @@ -4,7 +4,7 @@ -egse.tempcontrol.lakeshore.lsci336_ui API documentation +egse.tempcontrol.lakeshore.lsci_ui API documentation @@ -19,7 +19,7 @@
        -

        Module egse.tempcontrol.lakeshore.lsci336_ui

        +

        Module egse.tempcontrol.lakeshore.lsci_ui

        A Graphical User Interface for monitoring the LakeShore Temperature Controller.

        @@ -40,17 +40,15 @@

        Module egse.tempcontrol.lakeshore.lsci336_ui

        """ import argparse import logging -from pathlib import Path - -import sys import pyqtgraph as pg +import sys from PyQt5 import QtGui -from PyQt5.QtCore import QDateTime, QLockFile +from PyQt5.QtCore import QDateTime from PyQt5.QtCore import QTimer from PyQt5.QtCore import Qt from PyQt5.QtGui import QPalette, QIcon -from PyQt5.QtWidgets import QApplication, QMessageBox +from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QFrame from PyQt5.QtWidgets import QGridLayout from PyQt5.QtWidgets import QGroupBox @@ -67,16 +65,16 @@

        Module egse.tempcontrol.lakeshore.lsci336_ui

        from egse.resource import get_resource from egse.settings import Settings from egse.state import UnknownStateError -from egse.tempcontrol.lakeshore.lsci336 import LakeShore336Controller -from egse.tempcontrol.lakeshore.lsci336 import LakeShore336Proxy -from egse.tempcontrol.lakeshore.lsci336 import LakeShore336Simulator +from egse.tempcontrol.lakeshore.lsci import LakeShoreController +from egse.tempcontrol.lakeshore.lsci import LakeShoreProxy +from egse.tempcontrol.lakeshore.lsci import LakeShoreSimulator logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL) -logger = MODULE_LOGGER = logging.getLogger("LSCI-336 CS") +logger = MODULE_LOGGER = logging.getLogger("LSCI- CS") class LakeShoreUIView(QMainWindow, Observable): - def __init__(self): + def __init__(self,index_): super().__init__() # Define those variables that we will need/use in different methods @@ -85,11 +83,11 @@

        Module egse.tempcontrol.lakeshore.lsci336_ui

        self.clear_action = None self.reconnect_action = None self.toolbar = None - + self.index = index_ self.stripchart = None self.setGeometry(300, 300, 1000, 500) - self.setWindowTitle("LakeShore Controller") + self.setWindowTitle(f"LakeShore {self.index} Controller") self.init_gui() @@ -224,22 +222,22 @@

        Module egse.tempcontrol.lakeshore.lsci336_ui

        class LakeShoreUIModel: - def __init__(self, type_): + def __init__(self, type_, index_): self.type = type_ - + self.index = index_ if self.type == "proxy": - self.ls = LakeShore336Proxy() + self.ls = LakeShoreProxy(self.index) elif self.type == "direct": - self.ls = LakeShore336Controller() + self.ls = LakeShoreController() elif self.type == "simulator": - self.ls = LakeShore336Simulator() + self.ls = LakeShoreSimulator() else: raise ValueError( f"Unknown type of LakeShore implementation passed into the model: {type}" ) if self.ls is not None: - logger.debug(f"LakeShoreController initialized as {self.ls.__class__.__name__}") + logger.debug(f"LakeShoreController {self.index} initialized as {self.ls.__class__.__name__}") def is_simulator(self): return self.ls.is_simulator() @@ -258,12 +256,13 @@

        Module egse.tempcontrol.lakeshore.lsci336_ui

        Returns: the current temperature as a float or nan if no temperature available. """ - temperature = self.ls.get_temperature("A") - logger.debug(f"Temperature = {temperature:.3f}") + temperature = self.ls.get_temperature() if temperature is None: return float("nan") else: - return float(temperature) + temperature = float(temperature) + logger.debug(f"Temperature = {temperature:.3f}") + return temperature class LakeShoreUIController(Observer): @@ -294,19 +293,13 @@

        Module egse.tempcontrol.lakeshore.lsci336_ui

        # This is only needed when the Timer needs to run in another Thread # self.states_capture_timer.moveToThread(self) self.states_capture_timer.timeout.connect(self.update_values) - self.states_capture_timer.setInterval(200) - - self.stripchart_timer = QTimer() - self.stripchart_timer.timeout.connect(self.update_stripchart) - self.stripchart_timer.setInterval(200) + self.states_capture_timer.setInterval(300) def start_timer(self): self.states_capture_timer.start() - self.stripchart_timer.start() def stop_timer(self): self.states_capture_timer.stop() - self.stripchart_timer.stop() def update_values(self): @@ -314,10 +307,6 @@

        Module egse.tempcontrol.lakeshore.lsci336_ui

        temperature = self.model.get_temperature() self.view.update_temperature_field(temperature) - - def update_stripchart(self): - - temperature = self.model.get_temperature() self.view.update_stripchart(QDateTime.currentMSecsSinceEpoch(), temperature) def update(self, changed_object): @@ -364,6 +353,14 @@

        Module egse.tempcontrol.lakeshore.lsci336_ui

        Prepare the arguments that are specific for this application. """ parser = argparse.ArgumentParser() + + parser.add_argument( + "--index", + action="store", + help="Select LakeShore index to connected", + default="1", + ) + parser.add_argument( "--type", dest="type", @@ -378,12 +375,13 @@

        Module egse.tempcontrol.lakeshore.lsci336_ui

        action="store_true", help="Enable info logging messages with method profile information.", ) + args = parser.parse_args() return args def main(): - lock_file = QLockFile(str(Path("~/lsci336_ui.app.lock").expanduser())) + #lock_file = QLockFile(str(Path("~/lsci_ui.app.lock").expanduser())) STYLES_LOCATION = find_file("styles.qss", in_dir="egse/gui") args = list(sys.argv) @@ -392,42 +390,31 @@

        Module egse.tempcontrol.lakeshore.lsci336_ui

        app = QApplication(args) app.setWindowIcon(QIcon(str(get_resource(":/icons/temperature-control.svg")))) - if lock_file.tryLock(100): - - args = parse_arguments() - - if args.profile: - Settings.set_profiling(True) - if args.type == "proxy": - proxy = LakeShore336Proxy() - if not proxy.ping(): - description = "Could not connect to Temperature Control Control Server" - info_text = ( - "The GUI will start, but the connection button will show a disconnected state. " - "Please check if the Control Server is running and start the server if needed. " - "Otherwise, check if the correct HOSTNAME for the control server is set in the " - "Settings.yaml " - "configuration file." - ) + args = parse_arguments() - show_warning_message(description, info_text) + if args.profile: + Settings.set_profiling(True) - view = LakeShoreUIView() - model = LakeShoreUIModel(args.type) - controller = LakeShoreUIController(model, view) - - view.show() - sys.exit(app.exec_()) - else: - error_message = QMessageBox() - error_message.setIcon(QMessageBox.Warning) - error_message.setWindowTitle("Error") - error_message.setText("The LSCI336 tempcontrol GUI application is already running!") - error_message.setStandardButtons(QMessageBox.Ok) + if args.type == "proxy": + proxy = LakeShoreProxy(args.index) + if not proxy.ping(): + description = "Could not connect to Temperature Control Control Server" + info_text = ( + "The GUI will start, but the connection button will show a disconnected state. " + "Please check if the Control Server is running and start the server if needed. " + "Otherwise, check if the correct HOSTNAME for the control server is set in the " + "Settings.yaml " + "configuration file." + ) - return error_message.exec() + show_warning_message(description, info_text) + view = LakeShoreUIView(args.index) + model = LakeShoreUIModel(args.type,args.index) + controller = LakeShoreUIController(model, view) + view.show() + sys.exit(app.exec_()) if __name__ == "__main__": @@ -441,7 +428,7 @@

        Module egse.tempcontrol.lakeshore.lsci336_ui

        Functions

        -
        +
        def main()
        @@ -451,7 +438,7 @@

        Functions

        Expand source code
        def main():
        -    lock_file = QLockFile(str(Path("~/lsci336_ui.app.lock").expanduser()))
        +    #lock_file = QLockFile(str(Path("~/lsci_ui.app.lock").expanduser()))
             STYLES_LOCATION = find_file("styles.qss", in_dir="egse/gui")
         
             args = list(sys.argv)
        @@ -460,44 +447,34 @@ 

        Functions

        app = QApplication(args) app.setWindowIcon(QIcon(str(get_resource(":/icons/temperature-control.svg")))) - if lock_file.tryLock(100): - args = parse_arguments() + args = parse_arguments() - if args.profile: - Settings.set_profiling(True) + if args.profile: + Settings.set_profiling(True) - if args.type == "proxy": - proxy = LakeShore336Proxy() - if not proxy.ping(): - description = "Could not connect to Temperature Control Control Server" - info_text = ( - "The GUI will start, but the connection button will show a disconnected state. " - "Please check if the Control Server is running and start the server if needed. " - "Otherwise, check if the correct HOSTNAME for the control server is set in the " - "Settings.yaml " - "configuration file." - ) - - show_warning_message(description, info_text) - - view = LakeShoreUIView() - model = LakeShoreUIModel(args.type) - controller = LakeShoreUIController(model, view) + if args.type == "proxy": + proxy = LakeShoreProxy(args.index) + if not proxy.ping(): + description = "Could not connect to Temperature Control Control Server" + info_text = ( + "The GUI will start, but the connection button will show a disconnected state. " + "Please check if the Control Server is running and start the server if needed. " + "Otherwise, check if the correct HOSTNAME for the control server is set in the " + "Settings.yaml " + "configuration file." + ) - view.show() - sys.exit(app.exec_()) - else: - error_message = QMessageBox() - error_message.setIcon(QMessageBox.Warning) - error_message.setWindowTitle("Error") - error_message.setText("The LSCI336 tempcontrol GUI application is already running!") - error_message.setStandardButtons(QMessageBox.Ok) + show_warning_message(description, info_text) + view = LakeShoreUIView(args.index) + model = LakeShoreUIModel(args.type,args.index) + controller = LakeShoreUIController(model, view) - return error_message.exec()
        + view.show() + sys.exit(app.exec_())
        -
        +
        def parse_arguments()
        @@ -511,6 +488,14 @@

        Functions

        Prepare the arguments that are specific for this application. """ parser = argparse.ArgumentParser() + + parser.add_argument( + "--index", + action="store", + help="Select LakeShore index to connected", + default="1", + ) + parser.add_argument( "--type", dest="type", @@ -525,6 +510,7 @@

        Functions

        action="store_true", help="Enable info logging messages with method profile information.", ) + args = parser.parse_args() return args
        @@ -534,7 +520,7 @@

        Functions

        Classes

        -
        +
        class LakeShoreUIController (model, view)
        @@ -573,19 +559,13 @@

        Classes

        # This is only needed when the Timer needs to run in another Thread # self.states_capture_timer.moveToThread(self) self.states_capture_timer.timeout.connect(self.update_values) - self.states_capture_timer.setInterval(200) - - self.stripchart_timer = QTimer() - self.stripchart_timer.timeout.connect(self.update_stripchart) - self.stripchart_timer.setInterval(200) + self.states_capture_timer.setInterval(300) def start_timer(self): self.states_capture_timer.start() - self.stripchart_timer.start() def stop_timer(self): self.states_capture_timer.stop() - self.stripchart_timer.stop() def update_values(self): @@ -593,10 +573,6 @@

        Classes

        temperature = self.model.get_temperature() self.view.update_temperature_field(temperature) - - def update_stripchart(self): - - temperature = self.model.get_temperature() self.view.update_stripchart(QDateTime.currentMSecsSinceEpoch(), temperature) def update(self, changed_object): @@ -644,7 +620,7 @@

        Ancestors

      Methods

      -
      +
      def create_timer(self)
      @@ -660,14 +636,10 @@

      Methods

      # This is only needed when the Timer needs to run in another Thread # self.states_capture_timer.moveToThread(self) self.states_capture_timer.timeout.connect(self.update_values) - self.states_capture_timer.setInterval(200) - - self.stripchart_timer = QTimer() - self.stripchart_timer.timeout.connect(self.update_stripchart) - self.stripchart_timer.setInterval(200)
      + self.states_capture_timer.setInterval(300)
      -
      +
      def do(self, actions)
      @@ -686,7 +658,7 @@

      Methods

      self.model.goto(axis, position)
      -
      +
      def start_timer(self)
      @@ -696,11 +668,10 @@

      Methods

      Expand source code
      def start_timer(self):
      -    self.states_capture_timer.start()
      -    self.stripchart_timer.start()
      + self.states_capture_timer.start()
      -
      +
      def stop_timer(self)
      @@ -710,11 +681,10 @@

      Methods

      Expand source code
      def stop_timer(self):
      -    self.states_capture_timer.stop()
      -    self.stripchart_timer.stop()
      + self.states_capture_timer.stop()
      -
      +
      def update(self, changed_object)
      @@ -753,22 +723,7 @@

      Methods

      pass
      -
      -def update_stripchart(self) -
      -
      -
      -
      - -Expand source code - -
      def update_stripchart(self):
      -
      -    temperature = self.model.get_temperature()
      -    self.view.update_stripchart(QDateTime.currentMSecsSinceEpoch(), temperature)
      -
      -
      -
      +
      def update_values(self)
      @@ -782,14 +737,15 @@

      Methods

      # This method should not be called when not connected, by design! temperature = self.model.get_temperature() - self.view.update_temperature_field(temperature)
      + self.view.update_temperature_field(temperature) + self.view.update_stripchart(QDateTime.currentMSecsSinceEpoch(), temperature)
    -
    +
    class LakeShoreUIModel -(type_) +(type_, index_)
    @@ -798,22 +754,22 @@

    Methods

    Expand source code
    class LakeShoreUIModel:
    -    def __init__(self, type_):
    +    def __init__(self, type_, index_):
             self.type = type_
    -
    +        self.index = index_
             if self.type == "proxy":
    -            self.ls = LakeShore336Proxy()
    +            self.ls = LakeShoreProxy(self.index)
             elif self.type == "direct":
    -            self.ls = LakeShore336Controller()
    +            self.ls = LakeShoreController()
             elif self.type == "simulator":
    -            self.ls = LakeShore336Simulator()
    +            self.ls = LakeShoreSimulator()
             else:
                 raise ValueError(
                     f"Unknown type of LakeShore implementation passed into the model: {type}"
                 )
     
             if self.ls is not None:
    -            logger.debug(f"LakeShoreController initialized as {self.ls.__class__.__name__}")
    +            logger.debug(f"LakeShoreController {self.index} initialized as {self.ls.__class__.__name__}")
     
         def is_simulator(self):
             return self.ls.is_simulator()
    @@ -832,16 +788,17 @@ 

    Methods

    Returns: the current temperature as a float or nan if no temperature available. """ - temperature = self.ls.get_temperature("A") - logger.debug(f"Temperature = {temperature:.3f}") + temperature = self.ls.get_temperature() if temperature is None: return float("nan") else: - return float(temperature)
    + temperature = float(temperature) + logger.debug(f"Temperature = {temperature:.3f}") + return temperature

    Methods

    -
    +
    def get_temperature(self)
    @@ -858,15 +815,16 @@

    Returns

    Returns: the current temperature as a float or nan if no temperature available. """ - temperature = self.ls.get_temperature("A") - logger.debug(f"Temperature = {temperature:.3f}") + temperature = self.ls.get_temperature() if temperature is None: return float("nan") else: - return float(temperature)
    + temperature = float(temperature) + logger.debug(f"Temperature = {temperature:.3f}") + return temperature
    -
    +
    def is_connected(self)
    @@ -884,7 +842,7 @@

    Returns

    return return_code
    -
    +
    def is_simulator(self)
    @@ -899,17 +857,18 @@

    Returns

    -
    +
    class LakeShoreUIView +(index_)
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Expand source code
    class LakeShoreUIView(QMainWindow, Observable):
    -    def __init__(self):
    +    def __init__(self,index_):
             super().__init__()
     
             # Define those variables that we will need/use in different methods
    @@ -918,11 +877,11 @@ 

    Returns

    self.clear_action = None self.reconnect_action = None self.toolbar = None - + self.index = index_ self.stripchart = None self.setGeometry(300, 300, 1000, 500) - self.setWindowTitle("LakeShore Controller") + self.setWindowTitle(f"LakeShore {self.index} Controller") self.init_gui() @@ -1067,7 +1026,7 @@

    Ancestors

    Methods

    -
    +
    def create_channel_group(self)
    @@ -1117,7 +1076,7 @@

    Methods

    return group_box
    -
    +
    def init_gui(self)
    @@ -1147,7 +1106,7 @@

    Methods

    self.setCentralWidget(app_frame)
    -
    +
    def set_connection_state(self, state)
    @@ -1168,7 +1127,7 @@

    Methods

    )
    -
    +
    def set_style(self)
    @@ -1207,7 +1166,7 @@

    Methods

    )
    -
    +
    def set_style_simulator(self)
    @@ -1246,7 +1205,7 @@

    Methods

    )
    -
    +
    def update_stripchart(self, time, temperature)
    @@ -1259,7 +1218,7 @@

    Methods

    self.stripchart.update(time, temperature)
    -
    +
    def update_temperature_field(self, temperature)
    @@ -1290,42 +1249,41 @@

    Index

  • Functions

  • Classes

    diff --git a/docs/api/egse/tempcontrol/spid/spid_controller.html b/docs/api/egse/tempcontrol/spid/spid_controller.html deleted file mode 100644 index 8332a16..0000000 --- a/docs/api/egse/tempcontrol/spid/spid_controller.html +++ /dev/null @@ -1,921 +0,0 @@ - - - - - - -egse.tempcontrol.spid.spid_controller API documentation - - - - - - - - - - - -
    -
    -
    -

    Module egse.tempcontrol.spid.spid_controller

    -
    -
    -
    - -Expand source code - -
    import logging
    -import threading
    -
    -import time
    -
    -from egse.hk import get_housekeeping
    -from egse.state import GlobalState
    -from egse.tempcontrol.beaglebone.beaglebone import BeagleboneProxy
    -from egse.tempcontrol.spid.spid import PidInterface, PidError
    -
    -logger = logging.getLogger(__name__)
    -
    -
    -class PidController(PidInterface):
    -
    -    # Prefixes for prometheus metrics based on agilent device index
    -    PREFIXES = ["GSRON_AG34972_0_T", "GSRON_AG34972_1_T", "GSRON_AG34970_0_T", "GSRON_AG34970_1_T"]
    -
    -    def __init__(self):
    -
    -        # Get spid setup entry
    -        setup = GlobalState.setup
    -
    -        heaters = setup.gse.spid.configuration.heaters
    -
    -        self.channels = [channel for heater in heaters.values() for channel in heater]
    -
    -        self.update_period = setup.gse.spid.configuration.update_period
    -        self.temperature_timeout = setup.gse.spid.configuration.temperature_timeout
    -        self.timeout_action = setup.gse.spid.configuration.timeout_action
    -
    -        self._is_connected = False
    -        self._pid_thread = None
    -        self._beaglebone = BeagleboneProxy()
    -        self._t0 = None
    -
    -        self.setpoints = [float(0)] * len(self.channels)
    -        self.running = [False] * len(self.channels)
    -        self.pids = []
    -
    -        # Store these values for HK
    -        self.inputs         = [float(0)] * len(self.channels)
    -        self.errors         = [float(0)] * len(self.channels)
    -        self.outputs        = [float(0)] * len(self.channels)
    -        self.temperature    = [float(0)] * len(self.channels)
    -        self.timestamp      = [float(0)] * len(self.channels)
    -        self.isum           = [float(0)] * len(self.channels)
    -
    -        for i in range(len(self.channels)):
    -            self.pids.append( # initialize with parameters from setup
    -                PidAlgorithm(self.channels[i][5], self.channels[i][6], 0))
    -
    -        self.connect()
    -
    -
    -    def connect(self):
    -
    -        if self._is_connected:
    -            logger.warning("Trying to connect to an already connected device")
    -        else:
    -            self._beaglebone.connect()
    -            self._t0 = time.time()
    -            self._pid_thread = threading.Thread(target=self.thread_function, daemon=True)
    -            self._pid_thread.daemon = True
    -            self._is_connected = True
    -            self._pid_thread.start()
    -
    -
    -    def disconnect(self):
    -
    -        if not self._is_connected:
    -            logger.warning("Trying to disconnect to an already disconnected device")
    -        else:
    -            # NOTE: right now the time to stop the thread could take the entire update period
    -            self._is_connected = False
    -            self._pid_thread.join()
    -            self._beaglebone.disconnect()
    -
    -
    -    def reconnect(self):
    -
    -        if self._is_connected:
    -            self.disconnect()
    -        self.connect()
    -
    -
    -    def is_connected(self):
    -
    -        return self._is_connected
    -
    -
    -    def is_simulator(self):
    -
    -        return False
    -
    -    def get_pid_parameters(self, channel):
    -        Kp, Ki, Kd, int_max, int_min = self.pids[channel].Kp, self.pids[channel].Ki, self.pids[channel].Kd, self.pids[channel].int_max, self.pids[channel].int_min
    -        logger.info(f"Current PID parameters for channel {channel}:\n\
    -                      Kp: {Kp}, Ki: {Ki}, Kd: {Kd}\n\
    -                      Integral max: {int_max}\n\
    -                      Integral min: {int_min}")
    -        return Kp, Ki, Kd, int_max, int_min
    -    
    -    def set_pid_parameters(self, channel, Kp, Ki, Kd, int_max=1000, int_min=0, reset=True):
    -        logger.info(f"Setting PID parameters for channel {channel}:\n\
    -                      Kp: {Kp}, Ki: {Ki}, Kd: {Kd}\n\
    -                      Integral max: {int_max}\n\
    -                      Integral min: {int_min}\n\
    -                      Reset: {reset}")
    -        
    -        self.pids[channel].Kp = Kp
    -        self.pids[channel].Ki = Ki
    -        self.pids[channel].Kd = Kd
    -        self.pids[channel].int_min = int_min
    -        self.pids[channel].int_max = int_max
    -        if reset:
    -            self.pids[channel].intergral = 0
    -            self.pids[channel].prev_error = 0
    -
    -    def set_temperature(self, channel, setpoint):
    -
    -        logger.info(f'set PID {channel} to {setpoint}C')
    -
    -        if channel not in range(len(self.channels)):
    -            raise PidError(f'channel number {channel} is not defined')
    -
    -        self.setpoints[channel] = setpoint
    -
    -
    -    def get_temperature(self, channel):
    -        if channel not in range(len(self.channels)):
    -            raise PidError(f'channel number {channel} is not defined')
    -
    -        return self.setpoints[channel]
    -
    -
    -    def enable(self, channel):
    -        logger.info(f'PID {channel} has been enabled')
    -
    -        if channel not in range(len(self.channels)):
    -            raise PidError(f'channel number {channel} is not defined')
    -
    -        self.running[channel] = True
    -
    -
    -    def disable(self, channel):
    -        logger.info(f'PID {channel} has been disabled')
    -
    -        if channel not in range(len(self.channels)):
    -            raise PidError(f'channel number {channel} is not defined')
    -
    -        self.running[channel] = False
    -        _, _, _, htr_idx, ch_idx, _, _, _ = self.channels[channel]
    -        self._beaglebone.set_enable(htr_idx, ch_idx, False)
    -        
    -    def get_running(self, channel):
    -        return self.running[channel]
    -
    -
    -    def thread_function(self):
    -
    -        while self._is_connected:
    -
    -            for i, (_, agilent_index, agilent_channel, bb_index, bb_channel, _, _, _) \
    -                    in enumerate(self.channels):
    -
    -                # skip uninitialized channels
    -                if self.setpoints[i] is float('nan'):
    -                    continue
    -
    -                # Don't step the PID for unenabled channels to avoid a large I sum
    -                if not self.running[i]:
    -                    continue
    -
    -                # construct prometheus metric name
    -                metric_name = self.PREFIXES[agilent_index] + str(agilent_channel)
    -
    -                # get temperature from prometheus
    -                try:
    -                    self.timestamp[i], self.temperature[i] = get_housekeeping(f'{metric_name}')
    -
    -                except Exception as e:
    -                    logger.warning(f'could not get {metric_name} for pid loop {i} ({e})')
    -                    continue
    -
    -                # Check if the temperature is recently updated
    -                age = time.time() - self.timestamp[i]
    -                if age < self.temperature_timeout:
    -                    # Update the pid loop
    -                    self.inputs[i]  = float(self.temperature[i])
    -                    self.errors[i]  = self.setpoints[i] - self.inputs[i]
    -                    self.outputs[i] = self.pids[i].compute_step(self.errors[i])
    -                    self.isum[i]    = self.pids[i].integral
    -
    -                    # logger.info(f'PID {i}: error = {self.errors[i]:.2f}, '
    -                    #              f'output = {100 * self.outputs[i]:.2f}%')
    -                else:
    -                    logger.warning(f'pid loop {i} temperature outdated {metric_name} ({age}s)')
    -
    -                    if self.timeout_action == 'heater_off':
    -                        self.errors[i] = None
    -                        self.outputs[i] = 0
    -
    -                if self.outputs[i] == 0:
    -                    self._beaglebone.set_enable(bb_index, bb_channel, False)
    -                    # logger.info("PID output is zero")
    -
    -                else:
    -                    self._beaglebone.set_enable(bb_index, bb_channel, True)
    -
    -                    # logger.info(f'Set heater {bb_index}.{bb_channel} to {int(self.outputs[i] * 100)}%')
    -                    self._beaglebone.set_period(bb_index, bb_channel, 10000) # PWM period in ns
    -                    self._beaglebone.set_duty_cycle(
    -                        bb_index, bb_channel, int(self.outputs[i] * 10000)) # duty cycle in ns
    -
    -            # sleep for what is left of the update period
    -            t1 = time.time()
    -            dt = self.update_period - (t1 - self._t0)
    -            dt = max(dt, 0)
    -            time.sleep(dt)
    -            self._t0 = time.time()
    -
    -
    -class PidAlgorithm:
    -
    -    def __init__(self, Kp, Ki, Kd):
    -
    -        self.Kp = Kp
    -        self.Ki = Ki
    -        self.Kd = Kd
    -        logger.info(f"Kp: {self.Kp}, Ki: {self.Ki}, Kd: {self.Kd}")
    -        self.int_max = 1000
    -        self.int_min = 0
    -
    -        self.out_max = 0.9
    -        self.out_min = 0.0
    -
    -        self.prev_time = None
    -        self.integral = 0.0
    -        self.prev_error = 0.0
    -
    -
    -    def compute_step(self, error):
    -
    -        if self.prev_time is None:
    -            self.prev_time = time.time()
    -
    -        dt = time.time() - self.prev_time
    -        self.prev_time = time.time()
    -
    -        self.integral += error * dt
    -
    -        if self.integral > self.int_max:
    -            self.integral = self.int_max
    -
    -        if self.integral < self.int_min:
    -            self.integral = self.int_min
    -        
    -        derivative = (error - self.prev_error) / dt
    -        self.prev_error = error
    -
    -        output = self.Kp * error + self.Ki * self.integral + self.Kd * derivative
    -
    -        if output > self.out_max:
    -            output = self.out_max
    -
    -        if output < self.out_min:
    -            output = self.out_min
    -
    -        return output
    -
    -
    -def main():
    -    pid = PidController()
    -    # pid.set_temperature(2, -40)
    -
    -
    -if __name__ == '__main__':
    -    main()
    -
    -
    -
    -
    -
    -
    -
    -

    Functions

    -
    -
    -def main() -
    -
    -
    -
    - -Expand source code - -
    def main():
    -    pid = PidController()
    -    # pid.set_temperature(2, -40)
    -
    -
    -
    -
    -
    -

    Classes

    -
    -
    -class PidAlgorithm -(Kp, Ki, Kd) -
    -
    -
    -
    - -Expand source code - -
    class PidAlgorithm:
    -
    -    def __init__(self, Kp, Ki, Kd):
    -
    -        self.Kp = Kp
    -        self.Ki = Ki
    -        self.Kd = Kd
    -        logger.info(f"Kp: {self.Kp}, Ki: {self.Ki}, Kd: {self.Kd}")
    -        self.int_max = 1000
    -        self.int_min = 0
    -
    -        self.out_max = 0.9
    -        self.out_min = 0.0
    -
    -        self.prev_time = None
    -        self.integral = 0.0
    -        self.prev_error = 0.0
    -
    -
    -    def compute_step(self, error):
    -
    -        if self.prev_time is None:
    -            self.prev_time = time.time()
    -
    -        dt = time.time() - self.prev_time
    -        self.prev_time = time.time()
    -
    -        self.integral += error * dt
    -
    -        if self.integral > self.int_max:
    -            self.integral = self.int_max
    -
    -        if self.integral < self.int_min:
    -            self.integral = self.int_min
    -        
    -        derivative = (error - self.prev_error) / dt
    -        self.prev_error = error
    -
    -        output = self.Kp * error + self.Ki * self.integral + self.Kd * derivative
    -
    -        if output > self.out_max:
    -            output = self.out_max
    -
    -        if output < self.out_min:
    -            output = self.out_min
    -
    -        return output
    -
    -

    Methods

    -
    -
    -def compute_step(self, error) -
    -
    -
    -
    - -Expand source code - -
    def compute_step(self, error):
    -
    -    if self.prev_time is None:
    -        self.prev_time = time.time()
    -
    -    dt = time.time() - self.prev_time
    -    self.prev_time = time.time()
    -
    -    self.integral += error * dt
    -
    -    if self.integral > self.int_max:
    -        self.integral = self.int_max
    -
    -    if self.integral < self.int_min:
    -        self.integral = self.int_min
    -    
    -    derivative = (error - self.prev_error) / dt
    -    self.prev_error = error
    -
    -    output = self.Kp * error + self.Ki * self.integral + self.Kd * derivative
    -
    -    if output > self.out_max:
    -        output = self.out_max
    -
    -    if output < self.out_min:
    -        output = self.out_min
    -
    -    return output
    -
    -
    -
    -
    -
    -class PidController -
    -
    -

    PID base class.

    -
    - -Expand source code - -
    class PidController(PidInterface):
    -
    -    # Prefixes for prometheus metrics based on agilent device index
    -    PREFIXES = ["GSRON_AG34972_0_T", "GSRON_AG34972_1_T", "GSRON_AG34970_0_T", "GSRON_AG34970_1_T"]
    -
    -    def __init__(self):
    -
    -        # Get spid setup entry
    -        setup = GlobalState.setup
    -
    -        heaters = setup.gse.spid.configuration.heaters
    -
    -        self.channels = [channel for heater in heaters.values() for channel in heater]
    -
    -        self.update_period = setup.gse.spid.configuration.update_period
    -        self.temperature_timeout = setup.gse.spid.configuration.temperature_timeout
    -        self.timeout_action = setup.gse.spid.configuration.timeout_action
    -
    -        self._is_connected = False
    -        self._pid_thread = None
    -        self._beaglebone = BeagleboneProxy()
    -        self._t0 = None
    -
    -        self.setpoints = [float(0)] * len(self.channels)
    -        self.running = [False] * len(self.channels)
    -        self.pids = []
    -
    -        # Store these values for HK
    -        self.inputs         = [float(0)] * len(self.channels)
    -        self.errors         = [float(0)] * len(self.channels)
    -        self.outputs        = [float(0)] * len(self.channels)
    -        self.temperature    = [float(0)] * len(self.channels)
    -        self.timestamp      = [float(0)] * len(self.channels)
    -        self.isum           = [float(0)] * len(self.channels)
    -
    -        for i in range(len(self.channels)):
    -            self.pids.append( # initialize with parameters from setup
    -                PidAlgorithm(self.channels[i][5], self.channels[i][6], 0))
    -
    -        self.connect()
    -
    -
    -    def connect(self):
    -
    -        if self._is_connected:
    -            logger.warning("Trying to connect to an already connected device")
    -        else:
    -            self._beaglebone.connect()
    -            self._t0 = time.time()
    -            self._pid_thread = threading.Thread(target=self.thread_function, daemon=True)
    -            self._pid_thread.daemon = True
    -            self._is_connected = True
    -            self._pid_thread.start()
    -
    -
    -    def disconnect(self):
    -
    -        if not self._is_connected:
    -            logger.warning("Trying to disconnect to an already disconnected device")
    -        else:
    -            # NOTE: right now the time to stop the thread could take the entire update period
    -            self._is_connected = False
    -            self._pid_thread.join()
    -            self._beaglebone.disconnect()
    -
    -
    -    def reconnect(self):
    -
    -        if self._is_connected:
    -            self.disconnect()
    -        self.connect()
    -
    -
    -    def is_connected(self):
    -
    -        return self._is_connected
    -
    -
    -    def is_simulator(self):
    -
    -        return False
    -
    -    def get_pid_parameters(self, channel):
    -        Kp, Ki, Kd, int_max, int_min = self.pids[channel].Kp, self.pids[channel].Ki, self.pids[channel].Kd, self.pids[channel].int_max, self.pids[channel].int_min
    -        logger.info(f"Current PID parameters for channel {channel}:\n\
    -                      Kp: {Kp}, Ki: {Ki}, Kd: {Kd}\n\
    -                      Integral max: {int_max}\n\
    -                      Integral min: {int_min}")
    -        return Kp, Ki, Kd, int_max, int_min
    -    
    -    def set_pid_parameters(self, channel, Kp, Ki, Kd, int_max=1000, int_min=0, reset=True):
    -        logger.info(f"Setting PID parameters for channel {channel}:\n\
    -                      Kp: {Kp}, Ki: {Ki}, Kd: {Kd}\n\
    -                      Integral max: {int_max}\n\
    -                      Integral min: {int_min}\n\
    -                      Reset: {reset}")
    -        
    -        self.pids[channel].Kp = Kp
    -        self.pids[channel].Ki = Ki
    -        self.pids[channel].Kd = Kd
    -        self.pids[channel].int_min = int_min
    -        self.pids[channel].int_max = int_max
    -        if reset:
    -            self.pids[channel].intergral = 0
    -            self.pids[channel].prev_error = 0
    -
    -    def set_temperature(self, channel, setpoint):
    -
    -        logger.info(f'set PID {channel} to {setpoint}C')
    -
    -        if channel not in range(len(self.channels)):
    -            raise PidError(f'channel number {channel} is not defined')
    -
    -        self.setpoints[channel] = setpoint
    -
    -
    -    def get_temperature(self, channel):
    -        if channel not in range(len(self.channels)):
    -            raise PidError(f'channel number {channel} is not defined')
    -
    -        return self.setpoints[channel]
    -
    -
    -    def enable(self, channel):
    -        logger.info(f'PID {channel} has been enabled')
    -
    -        if channel not in range(len(self.channels)):
    -            raise PidError(f'channel number {channel} is not defined')
    -
    -        self.running[channel] = True
    -
    -
    -    def disable(self, channel):
    -        logger.info(f'PID {channel} has been disabled')
    -
    -        if channel not in range(len(self.channels)):
    -            raise PidError(f'channel number {channel} is not defined')
    -
    -        self.running[channel] = False
    -        _, _, _, htr_idx, ch_idx, _, _, _ = self.channels[channel]
    -        self._beaglebone.set_enable(htr_idx, ch_idx, False)
    -        
    -    def get_running(self, channel):
    -        return self.running[channel]
    -
    -
    -    def thread_function(self):
    -
    -        while self._is_connected:
    -
    -            for i, (_, agilent_index, agilent_channel, bb_index, bb_channel, _, _, _) \
    -                    in enumerate(self.channels):
    -
    -                # skip uninitialized channels
    -                if self.setpoints[i] is float('nan'):
    -                    continue
    -
    -                # Don't step the PID for unenabled channels to avoid a large I sum
    -                if not self.running[i]:
    -                    continue
    -
    -                # construct prometheus metric name
    -                metric_name = self.PREFIXES[agilent_index] + str(agilent_channel)
    -
    -                # get temperature from prometheus
    -                try:
    -                    self.timestamp[i], self.temperature[i] = get_housekeeping(f'{metric_name}')
    -
    -                except Exception as e:
    -                    logger.warning(f'could not get {metric_name} for pid loop {i} ({e})')
    -                    continue
    -
    -                # Check if the temperature is recently updated
    -                age = time.time() - self.timestamp[i]
    -                if age < self.temperature_timeout:
    -                    # Update the pid loop
    -                    self.inputs[i]  = float(self.temperature[i])
    -                    self.errors[i]  = self.setpoints[i] - self.inputs[i]
    -                    self.outputs[i] = self.pids[i].compute_step(self.errors[i])
    -                    self.isum[i]    = self.pids[i].integral
    -
    -                    # logger.info(f'PID {i}: error = {self.errors[i]:.2f}, '
    -                    #              f'output = {100 * self.outputs[i]:.2f}%')
    -                else:
    -                    logger.warning(f'pid loop {i} temperature outdated {metric_name} ({age}s)')
    -
    -                    if self.timeout_action == 'heater_off':
    -                        self.errors[i] = None
    -                        self.outputs[i] = 0
    -
    -                if self.outputs[i] == 0:
    -                    self._beaglebone.set_enable(bb_index, bb_channel, False)
    -                    # logger.info("PID output is zero")
    -
    -                else:
    -                    self._beaglebone.set_enable(bb_index, bb_channel, True)
    -
    -                    # logger.info(f'Set heater {bb_index}.{bb_channel} to {int(self.outputs[i] * 100)}%')
    -                    self._beaglebone.set_period(bb_index, bb_channel, 10000) # PWM period in ns
    -                    self._beaglebone.set_duty_cycle(
    -                        bb_index, bb_channel, int(self.outputs[i] * 10000)) # duty cycle in ns
    -
    -            # sleep for what is left of the update period
    -            t1 = time.time()
    -            dt = self.update_period - (t1 - self._t0)
    -            dt = max(dt, 0)
    -            time.sleep(dt)
    -            self._t0 = time.time()
    -
    -

    Ancestors

    - -

    Class variables

    -
    -
    var PREFIXES
    -
    -
    -
    -
    -

    Methods

    -
    -
    -def disable(self, channel) -
    -
    -
    -
    - -Expand source code - -
    def disable(self, channel):
    -    logger.info(f'PID {channel} has been disabled')
    -
    -    if channel not in range(len(self.channels)):
    -        raise PidError(f'channel number {channel} is not defined')
    -
    -    self.running[channel] = False
    -    _, _, _, htr_idx, ch_idx, _, _, _ = self.channels[channel]
    -    self._beaglebone.set_enable(htr_idx, ch_idx, False)
    -
    -
    -
    -def enable(self, channel) -
    -
    -
    -
    - -Expand source code - -
    def enable(self, channel):
    -    logger.info(f'PID {channel} has been enabled')
    -
    -    if channel not in range(len(self.channels)):
    -        raise PidError(f'channel number {channel} is not defined')
    -
    -    self.running[channel] = True
    -
    -
    -
    -def get_pid_parameters(self, channel) -
    -
    -
    -
    - -Expand source code - -
    def get_pid_parameters(self, channel):
    -    Kp, Ki, Kd, int_max, int_min = self.pids[channel].Kp, self.pids[channel].Ki, self.pids[channel].Kd, self.pids[channel].int_max, self.pids[channel].int_min
    -    logger.info(f"Current PID parameters for channel {channel}:\n\
    -                  Kp: {Kp}, Ki: {Ki}, Kd: {Kd}\n\
    -                  Integral max: {int_max}\n\
    -                  Integral min: {int_min}")
    -    return Kp, Ki, Kd, int_max, int_min
    -
    -
    -
    -def get_running(self, channel) -
    -
    -
    -
    - -Expand source code - -
    def get_running(self, channel):
    -    return self.running[channel]
    -
    -
    -
    -def get_temperature(self, channel) -
    -
    -
    -
    - -Expand source code - -
    def get_temperature(self, channel):
    -    if channel not in range(len(self.channels)):
    -        raise PidError(f'channel number {channel} is not defined')
    -
    -    return self.setpoints[channel]
    -
    -
    -
    -def set_pid_parameters(self, channel, Kp, Ki, Kd, int_max=1000, int_min=0, reset=True) -
    -
    -
    -
    - -Expand source code - -
    def set_pid_parameters(self, channel, Kp, Ki, Kd, int_max=1000, int_min=0, reset=True):
    -    logger.info(f"Setting PID parameters for channel {channel}:\n\
    -                  Kp: {Kp}, Ki: {Ki}, Kd: {Kd}\n\
    -                  Integral max: {int_max}\n\
    -                  Integral min: {int_min}\n\
    -                  Reset: {reset}")
    -    
    -    self.pids[channel].Kp = Kp
    -    self.pids[channel].Ki = Ki
    -    self.pids[channel].Kd = Kd
    -    self.pids[channel].int_min = int_min
    -    self.pids[channel].int_max = int_max
    -    if reset:
    -        self.pids[channel].intergral = 0
    -        self.pids[channel].prev_error = 0
    -
    -
    -
    -def thread_function(self) -
    -
    -
    -
    - -Expand source code - -
    def thread_function(self):
    -
    -    while self._is_connected:
    -
    -        for i, (_, agilent_index, agilent_channel, bb_index, bb_channel, _, _, _) \
    -                in enumerate(self.channels):
    -
    -            # skip uninitialized channels
    -            if self.setpoints[i] is float('nan'):
    -                continue
    -
    -            # Don't step the PID for unenabled channels to avoid a large I sum
    -            if not self.running[i]:
    -                continue
    -
    -            # construct prometheus metric name
    -            metric_name = self.PREFIXES[agilent_index] + str(agilent_channel)
    -
    -            # get temperature from prometheus
    -            try:
    -                self.timestamp[i], self.temperature[i] = get_housekeeping(f'{metric_name}')
    -
    -            except Exception as e:
    -                logger.warning(f'could not get {metric_name} for pid loop {i} ({e})')
    -                continue
    -
    -            # Check if the temperature is recently updated
    -            age = time.time() - self.timestamp[i]
    -            if age < self.temperature_timeout:
    -                # Update the pid loop
    -                self.inputs[i]  = float(self.temperature[i])
    -                self.errors[i]  = self.setpoints[i] - self.inputs[i]
    -                self.outputs[i] = self.pids[i].compute_step(self.errors[i])
    -                self.isum[i]    = self.pids[i].integral
    -
    -                # logger.info(f'PID {i}: error = {self.errors[i]:.2f}, '
    -                #              f'output = {100 * self.outputs[i]:.2f}%')
    -            else:
    -                logger.warning(f'pid loop {i} temperature outdated {metric_name} ({age}s)')
    -
    -                if self.timeout_action == 'heater_off':
    -                    self.errors[i] = None
    -                    self.outputs[i] = 0
    -
    -            if self.outputs[i] == 0:
    -                self._beaglebone.set_enable(bb_index, bb_channel, False)
    -                # logger.info("PID output is zero")
    -
    -            else:
    -                self._beaglebone.set_enable(bb_index, bb_channel, True)
    -
    -                # logger.info(f'Set heater {bb_index}.{bb_channel} to {int(self.outputs[i] * 100)}%')
    -                self._beaglebone.set_period(bb_index, bb_channel, 10000) # PWM period in ns
    -                self._beaglebone.set_duty_cycle(
    -                    bb_index, bb_channel, int(self.outputs[i] * 10000)) # duty cycle in ns
    -
    -        # sleep for what is left of the update period
    -        t1 = time.time()
    -        dt = self.update_period - (t1 - self._t0)
    -        dt = max(dt, 0)
    -        time.sleep(dt)
    -        self._t0 = time.time()
    -
    -
    -
    -

    Inherited members

    - -
    -
    -
    -
    - -
    - - - \ No newline at end of file diff --git a/docs/api/egse/tempcontrol/spid/spid_ui.html b/docs/api/egse/tempcontrol/spid/spid_ui.html deleted file mode 100644 index 160e190..0000000 --- a/docs/api/egse/tempcontrol/spid/spid_ui.html +++ /dev/null @@ -1,2375 +0,0 @@ - - - - - - -egse.tempcontrol.spid.spid_ui API documentation - - - - - - - - - - - -
    -
    -
    -

    Module egse.tempcontrol.spid.spid_ui

    -
    -
    -
    - -Expand source code - -
    from pathlib import Path
    -
    -import sys
    -import argparse
    -import logging
    -import multiprocessing
    -import zmq
    -import pickle
    -
    -import numpy as np
    -from zmq import ZMQError
    -from datetime import datetime
    -
    -from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot, Qt, QSize, QLockFile
    -
    -from PyQt5.QtGui import QCloseEvent
    -from PyQt5.QtWidgets import QMainWindow, QApplication, QLabel, QFrame, QHBoxLayout, QVBoxLayout, QGroupBox, QVBoxLayout, QGridLayout, \
    -    QLineEdit, QSizePolicy, QSpacerItem, QPushButton, QDoubleSpinBox, QScrollArea, QMessageBox
    -
    -from egse.tempcontrol.spid.spid import PidProxy, PidSimulator
    -from egse.tempcontrol.spid.spid import CTRL_SETTINGS as PID_CTRL_SETTINGS
    -
    -from egse.setup import get_setup
    -from egse.gui.led import LED
    -from egse.gui.buttons import ToggleButton
    -from egse.resource import get_resource
    -from egse.zmq_ser import connect_address
    -from egse.observer import Observer, Observable
    -
    -multiprocessing.current_process().name = 'spid_ui'
    -
    -logger = logging.getLogger(__name__)
    -
    -class PidMonitoringWorker(QObject):
    -    
    -    pid_timestamp_status_signal = pyqtSignal(list)
    -    pid_update_status_signal = pyqtSignal(list)
    -    pid_enabled_status_signal = pyqtSignal(dict)
    -    pid_setpoint_status_signal = pyqtSignal(list)
    -    pid_constants_status_signal = pyqtSignal(list)
    -    
    -    def __init__(self):
    -        super(PidMonitoringWorker, self).__init__()
    -        
    -        self.activate = False
    -        self.just_reconnected = True
    -        
    -        self.monitoring_socket = None
    -        self.is_socket_connected = True
    -        self.monitoring_timeout = 0.5
    -        
    -        self.connect_socket()
    -        
    -        self.previous_pid_update_status_signal = []
    -        self.previous_pid_timestamp_status_signal = []
    -        self.previous_pid_setpoint = []
    -        self.previous_pid_enabled_status_signal = []
    -        self.previous_pid_constants_status_signal = []
    -        
    -    def connect_socket(self):
    -        """ Create a socket and connect to the monitoring port.
    -        """
    -        
    -
    -        try:
    -            transport   = PID_CTRL_SETTINGS.PROTOCOL
    -            hostname    = PID_CTRL_SETTINGS.HOSTNAME
    -            
    -            monitoring_port = PID_CTRL_SETTINGS.MONITORING_PORT
    -            monitoring_address = connect_address(transport, hostname, monitoring_port)
    -            
    -            self.monitoring_socket = zmq.Context().socket(zmq.SUB)
    -            self.monitoring_socket.connect(monitoring_address)
    -            self.monitoring_socket.setsockopt_string(zmq.SUBSCRIBE, "")
    -            
    -            self.monitoring_timeout = 0.5
    -            
    -            self.is_socket_connected = True
    -            
    -        except:
    -            self.is_socket_connected = False
    -            
    -    def stop(self):
    -        
    -        """ Stop the monitoring worker.
    -        The monitoring socket is disconnected from the monitoring port and is then closed immediately.
    -        """
    -        
    -        self.monitoring_socket.close()
    -        self.is_socket_connected = False
    -        
    -        self.active = False
    -        
    -    def start_process(self):
    -        """Start updated the Beaglebone status"""
    -        self.run()
    -        
    -    @pyqtSlot()
    -    def run(self):
    -        """Keep on checkin whether the Beaglebone state has changed
    -        
    -        If the beaglebone status has changed, update it in the GUI
    -        Raises:
    -            Exception: ZMQ Error
    -        """
    -        
    -        self.active = True
    -        while self.is_socket_connected and self.active:
    -            
    -            try:
    -                socket_list, _, exc_list = zmq.select([self.monitoring_socket], [], [], timeout=self.monitoring_timeout)
    -
    -                if self.monitoring_socket in socket_list:
    -                    try:
    -                        pickle_string = self.monitoring_socket.recv()
    -                    except Exception:
    -                        raise Exception
    -
    -                    monitoring_info = pickle.loads(pickle_string)
    -                    
    -                    pid_enabled     = monitoring_info['Enabled']
    -                    pid_setpoint    = monitoring_info['Setpoint']
    -                    pid_timestamp   = monitoring_info['Timestamp']
    -                    pid_error       = monitoring_info['Error']
    -                    pid_isum        = monitoring_info['Isum']
    -                    pid_input       = monitoring_info['Input']
    -                    pid_output      = monitoring_info['Output']
    -                    pid_const       = monitoring_info['PidConst']
    -                    pid_state       = [pid_error, pid_isum, pid_input, pid_output]
    -                    
    -                    if pid_timestamp != self.previous_pid_timestamp_status_signal:
    -                        self.pid_timestamp_status_signal.emit(pid_timestamp)
    -                        
    -                    self.previous_pid_timestamp_status_signal = pid_timestamp
    -                    
    -                    if pid_setpoint != self.previous_pid_setpoint:
    -                        self.pid_setpoint_status_signal.emit(pid_setpoint)
    -                    
    -                    self.previous_pid_setpoint = pid_setpoint
    -                    
    -                    if pid_enabled != self.previous_pid_enabled_status_signal:
    -                        self.pid_enabled_status_signal.emit(pid_enabled)
    -                        
    -                    self.previous_pid_enabled_status_signal = pid_enabled
    -                    
    -                    if pid_state != self.previous_pid_update_status_signal:
    -                        self.pid_update_status_signal.emit(pid_state)
    -                    
    -                    self.previous_pid_update_status_signal = pid_state
    -                    
    -                    if pid_const != self.previous_pid_constants_status_signal:
    -                        self.pid_constants_status_signal.emit(pid_const)
    -                    
    -                    self.previous_pid_constants_status_signal = pid_const
    -                             
    -            except ZMQError as exc:
    -                raise exc
    -
    -
    -
    -class pidChannelWidget(QGroupBox):
    -    def __init__(self, channel_info, parent=None):
    -        super().__init__(parent=parent)
    -     
    -        
    -        self.pid = channel_info[0]
    -        self.htr = channel_info[3]
    -        self.ch  = channel_info[4]
    -        self.setObjectName("PID channel {} widget".format(self.ch))
    -        self.setMinimumWidth(380)
    -        sizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
    -        sizePolicy.setHorizontalStretch(0)
    -        sizePolicy.setVerticalStretch(0)
    -        sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth())
    -        self.setSizePolicy(sizePolicy)
    -        self.verticalLayout = QVBoxLayout(self)
    -        self.verticalLayout.setObjectName("verticalLayout")
    -        self.gridLayout = QGridLayout()
    -        self.gridLayout.setContentsMargins(0, -1, 0, -1)
    -        self.gridLayout.setObjectName("gridLayout")
    -        
    -        self.widget = ToggleButton(
    -            name="Turn On\Off PID Channel",
    -            status_tip=f"Heater {self.htr} Channel {self.ch} On\Off",
    -            selected=get_resource(":/icons/switch-on.svg"),
    -            not_selected=get_resource(":/icons/switch-off.svg"),
    -        )
    -        self.widget.setMinimumSize(QSize(35, 35))
    -        self.widget.setObjectName("widget")    
    -        self.gridLayout.addWidget(self.widget, 0, 0)
    -        self.label = QLabel(self)
    -        self.label.setMinimumSize(QSize(0, 0))
    -        self.label.setObjectName("label")
    -        self.label.setText("Heater {} Channel {}".format(int(self.htr + 1), chr(int(self.ch) + 65)))
    -        self.gridLayout.addWidget(self.label, 0, 1, 1, 5)
    -        self.label_2 = QLabel(self)
    -        self.label_2.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
    -        self.label_2.setObjectName("label_2")
    -        self.label_2.setText("Setpoint:")
    -        self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1)
    -        self.label_3 = QLabel(self)
    -        self.label_3.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
    -        self.label_3.setObjectName("label_3")
    -        self.label_3.setText("Input:")
    -        self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1)
    -        self.label_6 = QLabel(self)
    -        self.label_6.setObjectName("label_6")
    -        self.label_6.setText("Err:")
    -        self.gridLayout.addWidget(self.label_6, 2, 2, 1, 1)
    -        self.doubleSpinBox = QDoubleSpinBox(self)
    -        self.doubleSpinBox.setMinimumSize(QSize(90, 0))
    -        self.doubleSpinBox.setProperty("value", 21.0)
    -        self.doubleSpinBox.setObjectName("doubleSpinBox")
    -        self.doubleSpinBox.setMinimum(-250)
    -        self.gridLayout.addWidget(self.doubleSpinBox, 2, 1, 1, 1)
    -        self.label_13 = QLabel(self)
    -        self.label_13.setObjectName("label_13")
    -        self.label_13.setText("Timestamp:")
    -        self.gridLayout.addWidget(self.label_13, 5, 1, 1, 2)
    -        self.label_12 = QLabel(self)
    -        self.label_12.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
    -        self.label_12.setObjectName("label_12")
    -        self.label_12.setText("Out:")
    -        self.gridLayout.addWidget(self.label_12, 4, 0, 1, 1)
    -        self.label_7 = QLabel(self)
    -        self.label_7.setObjectName("label_7")
    -        self.label_7.setText("I:")
    -        self.gridLayout.addWidget(self.label_7, 3, 2, 1, 1)
    -        self.label_14 = QLabel(self)
    -        self.label_14.setObjectName("label_14")
    -        self.gridLayout.addWidget(self.label_14, 6, 1, 1, 4)
    -        self.lineEdit_3 = QLineEdit(self)
    -        self.lineEdit_3.setMinimumSize(QSize(90, 0))
    -        self.lineEdit_3.setReadOnly(True)
    -        self.lineEdit_3.setObjectName("lineEdit_3")
    -        self.gridLayout.addWidget(self.lineEdit_3, 3, 3, 1, 1)
    -        self.lineEdit_4 = QLineEdit(self)
    -        self.lineEdit_4.setMinimumSize(QSize(90, 0))
    -        self.lineEdit_4.setReadOnly(True)
    -        self.lineEdit_4.setObjectName("lineEdit_4")
    -        self.gridLayout.addWidget(self.lineEdit_4, 4, 1, 1, 1)
    -        self.lineEdit_2 = QLineEdit(self)
    -        self.lineEdit_2.setMinimumSize(QSize(90, 0))
    -        self.lineEdit_2.setReadOnly(True)
    -        self.lineEdit_2.setObjectName("lineEdit_2")
    -        self.gridLayout.addWidget(self.lineEdit_2, 3, 1, 1, 1)
    -        self.lineEdit = QLineEdit(self)
    -        self.lineEdit.setMinimumSize(QSize(90, 0))
    -        self.lineEdit.setReadOnly(True)
    -        self.lineEdit.setObjectName("lineEdit")
    -        self.gridLayout.addWidget(self.lineEdit, 2, 3, 1, 1)
    -        self.pushButton = QPushButton(self)
    -        self.pushButton.setMaximumSize(QSize(50, 16777215))
    -        self.pushButton.setObjectName("pushButton")
    -        self.pushButton.setText("Set")
    -        self.gridLayout.addWidget(self.pushButton, 2, 4, 1, 1)
    -        self.widget_2 = ToggleButton(
    -            width=10,
    -            height=10,
    -            name="More PID information",
    -            status_tip=f"Show more information about pid channel",
    -            selected=get_resource(":/icons/more.svg"),
    -            not_selected=get_resource(":/icons/arrow-up.svg")
    -        )
    -        self.widget_2.setObjectName("widget_2")
    -        self.gridLayout.addWidget(self.widget_2, 6, 5, 1, 1)
    -        self.horizontalLayout_2 = QHBoxLayout()
    -        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
    -        self.label_4 = QLabel(self)
    -        self.label_4.setObjectName("label_4")
    -        self.label_4.setText("Kp:")
    -        self.horizontalLayout_2.addWidget(self.label_4)
    -        self.label_5 = QLabel(self)
    -        self.label_5.setObjectName("label_5")
    -        self.label_5.setText("0")
    -        self.horizontalLayout_2.addWidget(self.label_5)
    -        self.label_8 = QLabel(self)
    -        self.label_8.setObjectName("label_8")
    -        self.label_8.setText("Ki:")
    -        self.horizontalLayout_2.addWidget(self.label_8)
    -        self.label_9 = QLabel(self)
    -        self.label_9.setObjectName("label_9")
    -        self.label_9.setText("0")
    -        self.horizontalLayout_2.addWidget(self.label_9)
    -        self.label_10 =QLabel(self)
    -        self.label_10.setObjectName("label_10")
    -        self.label_10.setText("Kd:")
    -        self.horizontalLayout_2.addWidget(self.label_10)
    -        self.label_11 = QLabel(self)
    -        self.label_11.setObjectName("label_11")
    -        self.label_11.setText("0")
    -        self.horizontalLayout_2.addWidget(self.label_11)
    -        self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 6)
    -        self.verticalLayout.addLayout(self.gridLayout)
    -
    -        self.widget_2.clicked.connect(self.show_more_less)
    -        
    -        self.show_less()
    -        
    -    def show_less(self):
    -        self.lineEdit_3.hide()
    -        self.lineEdit_4.hide()
    -        self.label_4.hide()
    -        self.label_5.hide()
    -        self.label_7.hide()
    -        self.label_8.hide()
    -        self.label_9.hide()
    -        self.label_10.hide()
    -        self.label_11.hide()
    -        self.label_12.hide()
    -    
    -    def show_more(self):
    -        self.lineEdit_3.show()
    -        self.lineEdit_4.show()
    -        self.label_4.show()
    -        self.label_5.show()
    -        self.label_7.show()
    -        self.label_8.show()
    -        self.label_9.show()
    -        self.label_10.show()
    -        self.label_11.show()
    -        self.label_12.show()
    -    
    -    def show_more_less(self):
    -        if not self.widget_2.is_selected():
    -            self.show_more()
    -        else:
    -            self.show_less()
    -
    -class pidGroupbox(QGroupBox):
    -    def __init__(self, pid_name, channels, parent=None):
    -        super().__init__(parent=parent)
    -        self.setObjectName("{}".format(pid_name))
    -        self.setMaximumWidth(400)
    -        self.verticalLayout = QVBoxLayout(self)
    -        self.verticalLayout.setObjectName("verticalLayout")
    -        self.horizontalLayout = QHBoxLayout()
    -        self.horizontalLayout.setObjectName("horizontalLayout")
    -        self.label = QLabel(self)
    -        self.label.setText(pid_name)
    -        sizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
    -        sizePolicy.setHorizontalStretch(0)
    -        sizePolicy.setVerticalStretch(0)
    -        sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
    -        self.label.setSizePolicy(sizePolicy)
    -        self.label.setObjectName("label")
    -        self.horizontalLayout.addWidget(self.label)
    -        
    -        self.widget_2 = ToggleButton(
    -            name="Turn On\Off PID Channel",
    -            status_tip=pid_name,
    -            selected=get_resource(":/icons/switch-on.svg"),
    -            not_selected=get_resource(":/icons/switch-off.svg"),
    -        )
    -        
    -        self.doubleSpinBox = QDoubleSpinBox(self)
    -        self.doubleSpinBox.setMinimumSize(QSize(90, 0))
    -        self.doubleSpinBox.setMinimum(-250)
    -        
    -        self.doubleSpinBox.setObjectName("doubleSpinBox")
    -        
    -        self.pushButton = QPushButton('set')
    -        self.pushButton.setMaximumSize(QSize(50, 16777215))
    -        
    -        self.horizontalLayout.addWidget(self.widget_2)
    -        spacerItem =QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
    -        self.horizontalLayout.addItem(spacerItem)
    -        self.horizontalLayout.addWidget(self.doubleSpinBox)
    -        self.horizontalLayout.addWidget(self.pushButton)
    -        
    -        
    -        self.widget = LED()
    -        self.widget.setMinimumSize(QSize(25, 25))
    -        self.widget.setObjectName("widget")
    -        self.horizontalLayout.addWidget(self.widget)
    -        self.verticalLayout.addLayout(self.horizontalLayout)
    -        self.channels = [None] * len(channels)
    -        for idx, channel in enumerate(channels):
    -            if (channel[1] == 0 and channel[2] == 0):
    -                continue
    -            self.channels[idx] = pidChannelWidget(channel)
    -            self.verticalLayout.addWidget(self.channels[idx])
    -        spacerItem_2 =QSpacerItem(40, 20, QSizePolicy.Minimum, QSizePolicy.Expanding)
    -        self.verticalLayout.addItem(spacerItem_2)
    -
    -class pidMainWindow(QMainWindow, Observable):
    -    def __init__(self, parent=None):
    -        super().__init__(parent=parent)
    -        self.setWindowTitle("Beaglebone PID Controller")
    -        self.setGeometry(600, 100, 1250, 670)
    -        self.setMaximumWidth(1250)
    -        
    -        setup = get_setup()
    -        self.pid_configuration = setup.gse.spid.configuration.heaters
    -        self.pid_configuration['MaRi'] = self.pid_configuration['TRP2'] + self.pid_configuration['TRP3'] + self.pid_configuration['TRP4']
    -        del self.pid_configuration['TRP2'], self.pid_configuration['TRP3'], self.pid_configuration['TRP4']
    -
    -        self.pid_monitoring_thread = QThread(self)
    -        self.pid_monitoring_worker = PidMonitoringWorker()
    -        self.pid_monitoring_worker.moveToThread(self.pid_monitoring_thread)
    -        
    -        self.pid_monitoring_worker.pid_enabled_status_signal.connect(self.on_pid_enabled_status_signal)
    -        self.pid_monitoring_worker.pid_setpoint_status_signal.connect(self.on_pid_setpoint_status_signal)
    -        self.pid_monitoring_worker.pid_timestamp_status_signal.connect(self.on_pid_timestamp_status_signal)
    -        self.pid_monitoring_worker.pid_update_status_signal.connect(self.on_pid_update_status_signal)
    -        self.pid_monitoring_worker.pid_constants_status_signal.connect(self.on_pid_constant_status_signal)
    -        
    -        self.pid_monitoring_thread.started.connect(self.pid_monitoring_worker.start_process)
    -        self.pid_monitoring_thread.start()
    -        
    -        self.initUI()
    -
    -    def on_pid_enabled_status_signal(self, monitoring_info:dict):
    -        monitoring_info = list(monitoring_info.values())
    -        
    -        for idx, (_, configuration) in enumerate(self.pid_configuration.items()):
    -            state_lin = len(self.pid_channels[idx].channels)
    -            pid_state = 0
    -            for ch_idx, ch in enumerate(configuration):
    -                if (ch[1] == 0 and ch[2] == 0):
    -                    state_lin -= 1
    -                    continue
    -                self.pid_channels[idx].channels[ch_idx].widget.set_selected(bool(monitoring_info[ch[0]]))
    -                ch_enabled = self.pid_channels[idx].channels[ch_idx].widget.is_selected()
    -                pid_state += 1 if int(ch_enabled) else 0
    -            if pid_state == state_lin:
    -                self.pid_channels[idx].widget.set_color(1)
    -                self.pid_channels[idx].widget_2.set_selected(True)
    -            elif  0 < pid_state < state_lin:
    -                self.pid_channels[idx].widget_2.set_selected(False)
    -                self.pid_channels[idx].widget.set_color(2)
    -            else:
    -                self.pid_channels[idx].widget_2.set_selected(False)
    -                self.pid_channels[idx].widget.set_color(0)
    -    
    -    def on_pid_constant_status_signal(self, monitoring_info):
    -        for idx, (_, configuration) in enumerate(self.pid_configuration.items()):
    -            for ch_idx, ch in enumerate(configuration):
    -                if (ch[1] == 0 and ch[2] == 0):
    -                    continue                
    -                self.pid_channels[idx].channels[ch_idx].label_5.setText(f"{monitoring_info[idx][0]:.2e}")
    -                self.pid_channels[idx].channels[ch_idx].label_9.setText(f"{monitoring_info[idx][1]:.2e}")  
    -                self.pid_channels[idx].channels[ch_idx].label_11.setText(f"{monitoring_info[idx][2]:.2e}")  
    -    
    -    def on_pid_setpoint_status_signal(self, monitoring_info):
    -        for idx, (_, configuration) in enumerate(self.pid_configuration.items()):
    -            setpoint = []
    -            for ch_idx, ch in enumerate(configuration):
    -                if (ch[1] == 0 and ch[2] == 0):
    -                    continue                
    -                self.pid_channels[idx].channels[ch_idx].doubleSpinBox.setValue(float(monitoring_info[ch[0]]))
    -                setpoint.append(float(monitoring_info[ch[0]]))
    -            self.pid_channels[idx].doubleSpinBox.setValue(float(np.amax(setpoint)))
    -                
    -    def on_pid_timestamp_status_signal(self, monitoring_info):
    -        for idx, (_, configuration) in enumerate(self.pid_configuration.items()):
    -            for ch_idx, ch in enumerate(configuration):
    -                if (ch[1] == 0 and ch[2] == 0):
    -                    continue
    -                self.pid_channels[idx].channels[ch_idx].label_14.setText(str(datetime.fromtimestamp(monitoring_info[int(ch[0])])))
    -    
    -    def on_pid_update_status_signal(self, monitoring_info):
    -        channels = 0
    -        for idx, (pid, configuration) in enumerate(self.pid_configuration.items()):
    -            for ch_idx, ch in enumerate(configuration):
    -                if (ch[1] == 0 and ch[2] == 0):
    -                    continue
    -                self.pid_channels[idx].channels[ch_idx].lineEdit.setText(f"{monitoring_info[0][channels]:.2e}")
    -                self.pid_channels[idx].channels[ch_idx].lineEdit_3.setText(f"{monitoring_info[1][channels]:.2e}")
    -                self.pid_channels[idx].channels[ch_idx].lineEdit_2.setText(f"{monitoring_info[2][channels]:.2e}")
    -                self.pid_channels[idx].channels[ch_idx].lineEdit_4.setText(f"{monitoring_info[3][channels]:.2e}")
    -                channels += 1
    -
    -    def initUI(self):
    -        scroll_frame = QScrollArea()
    -        scroll_frame.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
    -        scroll_frame.setWidgetResizable(True)
    -        app_frame = QFrame()
    -        grid_layout = QGridLayout()
    -        self.pid_channels = [None] * len(self.pid_configuration)
    -        for idx, (pid, configuration) in enumerate(self.pid_configuration.items()):
    -            configuration.sort( key = lambda configuration: (configuration[3], configuration[4]))
    -            self.pid_channels[idx] = pidGroupbox(pid, configuration)
    -            
    -            self.pid_channels[idx].pushButton.clicked.connect(self.set_all_pid_setpoint)
    -            self.pid_channels[idx].widget_2.clicked.connect(self.set_all_pid_control)
    -            
    -            grid_layout.addWidget(self.pid_channels[idx], int(idx / 3), idx % 3)
    -            for ch_idx, ch in enumerate(configuration):
    -                if (ch[1] == 0 and ch[2] == 0):
    -                    continue
    -                self.pid_channels[idx].channels[ch_idx].widget.clicked.connect(self.set_pid_control)
    -                self.pid_channels[idx].channels[ch_idx].pushButton.clicked.connect(self.set_pid_setpoint)
    -        app_frame.setLayout(grid_layout)
    -        scroll_frame.setWidget(app_frame)
    -        self.setCentralWidget(scroll_frame)
    -
    -    def set_all_pid_control(self):
    -        sender = self.sender()
    -        name = sender.parent().objectName()
    -        new_state = sender.is_selected()
    -        sender.set_selected(on=new_state)
    -        for channel in self.pid_configuration[name]:
    -            try:
    -                self.actionObservers({"set_pid_control" : [channel[0], bool(new_state)]})
    -            except Exception as e:
    -                warning_popup("set_pid_control : Channel {} {}".format(pid, "Enabled" 
    -                                                                        if new_state else "Disabled"), e)
    -    def set_pid_control(self):
    -        sender = self.sender()
    -        state = sender.is_selected()
    -        new_state = state
    -        sender.set_selected(on=new_state)
    -        pid = sender.parent().pid
    -        htr = sender.parent().htr
    -        ch  = sender.parent().ch
    -
    -        try:
    -            self.actionObservers({"set_pid_control" : [pid, bool(new_state)]})
    -        except Exception as e:
    -            warning_popup("set_pid_control : Channel {} {}".format(pid, "Enabled" 
    -                                                                   if new_state else "Disabled"), e)
    -    
    -    def set_all_pid_setpoint(self):
    -        sender = self.sender()
    -        setpoint = float(sender.parent().doubleSpinBox.value())
    -        name = sender.parent().objectName()
    -        
    -        for channel in self.pid_configuration[name]:
    -            try:
    -                self.actionObservers({"set_pid_setpoint" : [channel[0], float(setpoint)]})
    -            except Exception as e:
    -                 warning_popup("set_pid_control : Channel {} {}".format(pid, "Enabled" 
    -                                                                    if new_state else "Disabled"), e)
    -    
    -    def set_pid_setpoint(self):
    -        sender = self.sender()
    -        setpoint = float(sender.parent().doubleSpinBox.value())
    -        pid = sender.parent().pid
    -        htr = sender.parent().htr
    -        ch  = sender.parent().ch
    -
    -        try:
    -            self.actionObservers({"set_pid_setpoint" : [pid, float(setpoint)]})
    -        except Exception as e:
    -            warning_popup("set_pid_setpoint : PID Channel {} to {} C".format(pid, setpoint), e)      
    -              
    -    def closeEvent(self, close_Event: QCloseEvent) -> None:
    -        self.pid_monitoring_thread.quit()
    -        self.pid_monitoring_worker.stop()
    -
    -def warning_popup(command, error):
    -    msgBox = QMessageBox()
    -    msgBox.setWindowTitle("OGSE error occured")
    -    msgBox.setIcon(QMessageBox.Warning)
    -    msgBox.setText(f"An error occured while executing: {command}")
    -    msgBox.setInformativeText(f"{error}")
    -    msgBox.setStandardButtons(QMessageBox.Ok)
    -    msgBox.exec_()
    -
    -class PIDUIModel:
    -    def __init__(self, mode):
    -        self.mode = mode
    -        if self.mode == "proxy":
    -            try:
    -                self.pid = PidProxy()
    -            except Exception as exc:
    -                raise exc
    -            
    -        elif self.mode == 'simulator':
    -            self.pid = PidSimulator()
    -        else:
    -            raise ValueError(f'Unknown type of PID implementation passed into the model')
    -        
    -       
    -        if self.pid is not None:
    -                logger.debug(f'Heater Controller initialized as {self.pid.__class__.__name__}')
    -
    -    def set_pid_control(self, ch, state):
    -        with PidProxy() as pid:
    -            if state:
    -                pid.enable(int(ch))
    -            else:
    -                pid.disable(int(ch))
    -
    -    
    -    def set_pid_setpoint(self, ch, sp):
    -        with PidProxy() as pid:
    -            pid.set_temperature(int(ch), float(sp))
    -    
    -    def has_commands(self):
    -        if self.mode == 'proxy':
    -            with PidProxy() as pid:
    -                pid.has_commands()
    -            
    -        return True
    -                
    -    
    -    def load_commands(self):
    -        if self.mode == 'proxy':
    -            with PidProxy() as pid:
    -                pid.load_commands()
    -    
    -    def is_simulator(self):
    -        with PidProxy() as pid:
    -            pid.is_simulator()
    -
    -    
    -    def is_connected(self):
    -        with PidProxy() as pid:
    -            pid.is_cs_connected()
    -    
    -    def reconnect(self):
    -        if self.mode == "proxy":
    -            with PidProxy() as pid:
    -                pid.reconnect()
    -            return self.pid.is_cs_connected()
    -        return False
    -    
    -    def disconnect(self):
    -        if self.mode == "proxy":
    -            with PidProxy() as pid:
    -                return pid.disconnect_cs()
    -
    -        elif self.mode == 'simulator':
    -            return self.pid.disconnect()
    -
    -
    -class PIDUIController(Observer):
    -    def __init__(self, model: PIDUIModel, view: pidMainWindow):
    -        self.model  = model
    -        self.view   = view
    -        self.view.addObserver(self)
    -        
    -    def update(self, changed_object):
    -
    -        text = changed_object.text()
    -
    -        if text == "Reconnect":
    -
    -            if changed_object.isChecked():
    -
    -                logger.debug("Reconnecting the Heater model.")
    -
    -                if self.model.reconnect():
    -
    -                    self.view.set_connection_state(True)
    -
    -                    if not self.model.has_commands():
    -
    -                        self.model.load_commands()
    -
    -                else:
    -                    self.view.reconnect_action.setChecked(False)
    -            else:
    -
    -                logger.debug("Disconnecting the Heater model.")
    -                self.model.disconnect()
    -                self.view.set_connection_state(False)
    -
    -            return
    -        
    -    def do(self, actions):
    -        for action, value in actions.items():
    -            logger.debug(f"do {action} with {value}")
    -            if action == "set_pid_control":
    -                ch      = value[0]
    -                state   = value[1]
    -
    -                self.model.set_pid_control(ch, state)
    - 
    -            if action == "set_pid_setpoint":
    -                ch      = value[0]
    -                temp    = value[1]
    -                self.model.set_pid_setpoint(ch, temp)
    -        
    -def parse_arguments():
    -    """
    -    Prepare the arguments that are specific for this application.
    -    """
    -    parser = argparse.ArgumentParser()
    -    parser.add_argument(
    -        "--type",
    -        dest="type",
    -        action="store",
    -        choices={"proxy", "simulator"},
    -        help="Specify PID implementation you want to connect to.",
    -        default="proxy",
    -    )
    -
    -    args = parser.parse_args()
    -    return args
    -
    -
    -def main():
    -
    -    lock_file = QLockFile(str(Path("~/spid_ui.app.lock").expanduser()))
    -
    -    args = list(sys.argv)
    -    app = QApplication(args)
    -
    -    if lock_file.tryLock(100):
    -
    -        args = parse_arguments()
    -        if args.type == 'proxy':
    -            try:
    -                proxy = PidProxy()
    -            except Exception:
    -                raise Exception
    -        view = pidMainWindow()
    -        model = PIDUIModel(args.type)
    -        controller = PIDUIController(model, view)
    -        view.show()
    -        app.exec_()
    -    else:
    -        error_message = QMessageBox()
    -        error_message.setIcon(QMessageBox.Warning)
    -        error_message.setWindowTitle("Error")
    -        error_message.setText("The SPID tempcontrol GUI application is already running!")
    -        error_message.setStandardButtons(QMessageBox.Ok)
    -
    -        return error_message.exec()
    -
    -
    -if __name__ == "__main__":
    -    main()
    -        
    -
    -
    -
    -
    -
    -
    -
    -

    Functions

    -
    -
    -def main() -
    -
    -
    -
    - -Expand source code - -
    def main():
    -
    -    lock_file = QLockFile(str(Path("~/spid_ui.app.lock").expanduser()))
    -
    -    args = list(sys.argv)
    -    app = QApplication(args)
    -
    -    if lock_file.tryLock(100):
    -
    -        args = parse_arguments()
    -        if args.type == 'proxy':
    -            try:
    -                proxy = PidProxy()
    -            except Exception:
    -                raise Exception
    -        view = pidMainWindow()
    -        model = PIDUIModel(args.type)
    -        controller = PIDUIController(model, view)
    -        view.show()
    -        app.exec_()
    -    else:
    -        error_message = QMessageBox()
    -        error_message.setIcon(QMessageBox.Warning)
    -        error_message.setWindowTitle("Error")
    -        error_message.setText("The SPID tempcontrol GUI application is already running!")
    -        error_message.setStandardButtons(QMessageBox.Ok)
    -
    -        return error_message.exec()
    -
    -
    -
    -def parse_arguments() -
    -
    -

    Prepare the arguments that are specific for this application.

    -
    - -Expand source code - -
    def parse_arguments():
    -    """
    -    Prepare the arguments that are specific for this application.
    -    """
    -    parser = argparse.ArgumentParser()
    -    parser.add_argument(
    -        "--type",
    -        dest="type",
    -        action="store",
    -        choices={"proxy", "simulator"},
    -        help="Specify PID implementation you want to connect to.",
    -        default="proxy",
    -    )
    -
    -    args = parser.parse_args()
    -    return args
    -
    -
    -
    -def warning_popup(command, error) -
    -
    -
    -
    - -Expand source code - -
    def warning_popup(command, error):
    -    msgBox = QMessageBox()
    -    msgBox.setWindowTitle("OGSE error occured")
    -    msgBox.setIcon(QMessageBox.Warning)
    -    msgBox.setText(f"An error occured while executing: {command}")
    -    msgBox.setInformativeText(f"{error}")
    -    msgBox.setStandardButtons(QMessageBox.Ok)
    -    msgBox.exec_()
    -
    -
    -
    -
    -
    -

    Classes

    -
    -
    -class PIDUIController -(model: PIDUIModel, view: pidMainWindow) -
    -
    -

    Helper class that provides a standard way to create an ABC using -inheritance.

    -
    - -Expand source code - -
    class PIDUIController(Observer):
    -    def __init__(self, model: PIDUIModel, view: pidMainWindow):
    -        self.model  = model
    -        self.view   = view
    -        self.view.addObserver(self)
    -        
    -    def update(self, changed_object):
    -
    -        text = changed_object.text()
    -
    -        if text == "Reconnect":
    -
    -            if changed_object.isChecked():
    -
    -                logger.debug("Reconnecting the Heater model.")
    -
    -                if self.model.reconnect():
    -
    -                    self.view.set_connection_state(True)
    -
    -                    if not self.model.has_commands():
    -
    -                        self.model.load_commands()
    -
    -                else:
    -                    self.view.reconnect_action.setChecked(False)
    -            else:
    -
    -                logger.debug("Disconnecting the Heater model.")
    -                self.model.disconnect()
    -                self.view.set_connection_state(False)
    -
    -            return
    -        
    -    def do(self, actions):
    -        for action, value in actions.items():
    -            logger.debug(f"do {action} with {value}")
    -            if action == "set_pid_control":
    -                ch      = value[0]
    -                state   = value[1]
    -
    -                self.model.set_pid_control(ch, state)
    - 
    -            if action == "set_pid_setpoint":
    -                ch      = value[0]
    -                temp    = value[1]
    -                self.model.set_pid_setpoint(ch, temp)
    -
    -

    Ancestors

    - -

    Methods

    -
    -
    -def do(self, actions) -
    -
    -
    -
    - -Expand source code - -
    def do(self, actions):
    -    for action, value in actions.items():
    -        logger.debug(f"do {action} with {value}")
    -        if action == "set_pid_control":
    -            ch      = value[0]
    -            state   = value[1]
    -
    -            self.model.set_pid_control(ch, state)
    -
    -        if action == "set_pid_setpoint":
    -            ch      = value[0]
    -            temp    = value[1]
    -            self.model.set_pid_setpoint(ch, temp)
    -
    -
    -
    -def update(self, changed_object) -
    -
    -
    -
    - -Expand source code - -
    def update(self, changed_object):
    -
    -    text = changed_object.text()
    -
    -    if text == "Reconnect":
    -
    -        if changed_object.isChecked():
    -
    -            logger.debug("Reconnecting the Heater model.")
    -
    -            if self.model.reconnect():
    -
    -                self.view.set_connection_state(True)
    -
    -                if not self.model.has_commands():
    -
    -                    self.model.load_commands()
    -
    -            else:
    -                self.view.reconnect_action.setChecked(False)
    -        else:
    -
    -            logger.debug("Disconnecting the Heater model.")
    -            self.model.disconnect()
    -            self.view.set_connection_state(False)
    -
    -        return
    -
    -
    -
    -
    -
    -class PIDUIModel -(mode) -
    -
    -
    -
    - -Expand source code - -
    class PIDUIModel:
    -    def __init__(self, mode):
    -        self.mode = mode
    -        if self.mode == "proxy":
    -            try:
    -                self.pid = PidProxy()
    -            except Exception as exc:
    -                raise exc
    -            
    -        elif self.mode == 'simulator':
    -            self.pid = PidSimulator()
    -        else:
    -            raise ValueError(f'Unknown type of PID implementation passed into the model')
    -        
    -       
    -        if self.pid is not None:
    -                logger.debug(f'Heater Controller initialized as {self.pid.__class__.__name__}')
    -
    -    def set_pid_control(self, ch, state):
    -        with PidProxy() as pid:
    -            if state:
    -                pid.enable(int(ch))
    -            else:
    -                pid.disable(int(ch))
    -
    -    
    -    def set_pid_setpoint(self, ch, sp):
    -        with PidProxy() as pid:
    -            pid.set_temperature(int(ch), float(sp))
    -    
    -    def has_commands(self):
    -        if self.mode == 'proxy':
    -            with PidProxy() as pid:
    -                pid.has_commands()
    -            
    -        return True
    -                
    -    
    -    def load_commands(self):
    -        if self.mode == 'proxy':
    -            with PidProxy() as pid:
    -                pid.load_commands()
    -    
    -    def is_simulator(self):
    -        with PidProxy() as pid:
    -            pid.is_simulator()
    -
    -    
    -    def is_connected(self):
    -        with PidProxy() as pid:
    -            pid.is_cs_connected()
    -    
    -    def reconnect(self):
    -        if self.mode == "proxy":
    -            with PidProxy() as pid:
    -                pid.reconnect()
    -            return self.pid.is_cs_connected()
    -        return False
    -    
    -    def disconnect(self):
    -        if self.mode == "proxy":
    -            with PidProxy() as pid:
    -                return pid.disconnect_cs()
    -
    -        elif self.mode == 'simulator':
    -            return self.pid.disconnect()
    -
    -

    Methods

    -
    -
    -def disconnect(self) -
    -
    -
    -
    - -Expand source code - -
    def disconnect(self):
    -    if self.mode == "proxy":
    -        with PidProxy() as pid:
    -            return pid.disconnect_cs()
    -
    -    elif self.mode == 'simulator':
    -        return self.pid.disconnect()
    -
    -
    -
    -def has_commands(self) -
    -
    -
    -
    - -Expand source code - -
    def has_commands(self):
    -    if self.mode == 'proxy':
    -        with PidProxy() as pid:
    -            pid.has_commands()
    -        
    -    return True
    -
    -
    -
    -def is_connected(self) -
    -
    -
    -
    - -Expand source code - -
    def is_connected(self):
    -    with PidProxy() as pid:
    -        pid.is_cs_connected()
    -
    -
    -
    -def is_simulator(self) -
    -
    -
    -
    - -Expand source code - -
    def is_simulator(self):
    -    with PidProxy() as pid:
    -        pid.is_simulator()
    -
    -
    -
    -def load_commands(self) -
    -
    -
    -
    - -Expand source code - -
    def load_commands(self):
    -    if self.mode == 'proxy':
    -        with PidProxy() as pid:
    -            pid.load_commands()
    -
    -
    -
    -def reconnect(self) -
    -
    -
    -
    - -Expand source code - -
    def reconnect(self):
    -    if self.mode == "proxy":
    -        with PidProxy() as pid:
    -            pid.reconnect()
    -        return self.pid.is_cs_connected()
    -    return False
    -
    -
    -
    -def set_pid_control(self, ch, state) -
    -
    -
    -
    - -Expand source code - -
    def set_pid_control(self, ch, state):
    -    with PidProxy() as pid:
    -        if state:
    -            pid.enable(int(ch))
    -        else:
    -            pid.disable(int(ch))
    -
    -
    -
    -def set_pid_setpoint(self, ch, sp) -
    -
    -
    -
    - -Expand source code - -
    def set_pid_setpoint(self, ch, sp):
    -    with PidProxy() as pid:
    -        pid.set_temperature(int(ch), float(sp))
    -
    -
    -
    -
    -
    -class PidMonitoringWorker -
    -
    -

    QObject(parent: QObject = None)

    -
    - -Expand source code - -
    class PidMonitoringWorker(QObject):
    -    
    -    pid_timestamp_status_signal = pyqtSignal(list)
    -    pid_update_status_signal = pyqtSignal(list)
    -    pid_enabled_status_signal = pyqtSignal(dict)
    -    pid_setpoint_status_signal = pyqtSignal(list)
    -    pid_constants_status_signal = pyqtSignal(list)
    -    
    -    def __init__(self):
    -        super(PidMonitoringWorker, self).__init__()
    -        
    -        self.activate = False
    -        self.just_reconnected = True
    -        
    -        self.monitoring_socket = None
    -        self.is_socket_connected = True
    -        self.monitoring_timeout = 0.5
    -        
    -        self.connect_socket()
    -        
    -        self.previous_pid_update_status_signal = []
    -        self.previous_pid_timestamp_status_signal = []
    -        self.previous_pid_setpoint = []
    -        self.previous_pid_enabled_status_signal = []
    -        self.previous_pid_constants_status_signal = []
    -        
    -    def connect_socket(self):
    -        """ Create a socket and connect to the monitoring port.
    -        """
    -        
    -
    -        try:
    -            transport   = PID_CTRL_SETTINGS.PROTOCOL
    -            hostname    = PID_CTRL_SETTINGS.HOSTNAME
    -            
    -            monitoring_port = PID_CTRL_SETTINGS.MONITORING_PORT
    -            monitoring_address = connect_address(transport, hostname, monitoring_port)
    -            
    -            self.monitoring_socket = zmq.Context().socket(zmq.SUB)
    -            self.monitoring_socket.connect(monitoring_address)
    -            self.monitoring_socket.setsockopt_string(zmq.SUBSCRIBE, "")
    -            
    -            self.monitoring_timeout = 0.5
    -            
    -            self.is_socket_connected = True
    -            
    -        except:
    -            self.is_socket_connected = False
    -            
    -    def stop(self):
    -        
    -        """ Stop the monitoring worker.
    -        The monitoring socket is disconnected from the monitoring port and is then closed immediately.
    -        """
    -        
    -        self.monitoring_socket.close()
    -        self.is_socket_connected = False
    -        
    -        self.active = False
    -        
    -    def start_process(self):
    -        """Start updated the Beaglebone status"""
    -        self.run()
    -        
    -    @pyqtSlot()
    -    def run(self):
    -        """Keep on checkin whether the Beaglebone state has changed
    -        
    -        If the beaglebone status has changed, update it in the GUI
    -        Raises:
    -            Exception: ZMQ Error
    -        """
    -        
    -        self.active = True
    -        while self.is_socket_connected and self.active:
    -            
    -            try:
    -                socket_list, _, exc_list = zmq.select([self.monitoring_socket], [], [], timeout=self.monitoring_timeout)
    -
    -                if self.monitoring_socket in socket_list:
    -                    try:
    -                        pickle_string = self.monitoring_socket.recv()
    -                    except Exception:
    -                        raise Exception
    -
    -                    monitoring_info = pickle.loads(pickle_string)
    -                    
    -                    pid_enabled     = monitoring_info['Enabled']
    -                    pid_setpoint    = monitoring_info['Setpoint']
    -                    pid_timestamp   = monitoring_info['Timestamp']
    -                    pid_error       = monitoring_info['Error']
    -                    pid_isum        = monitoring_info['Isum']
    -                    pid_input       = monitoring_info['Input']
    -                    pid_output      = monitoring_info['Output']
    -                    pid_const       = monitoring_info['PidConst']
    -                    pid_state       = [pid_error, pid_isum, pid_input, pid_output]
    -                    
    -                    if pid_timestamp != self.previous_pid_timestamp_status_signal:
    -                        self.pid_timestamp_status_signal.emit(pid_timestamp)
    -                        
    -                    self.previous_pid_timestamp_status_signal = pid_timestamp
    -                    
    -                    if pid_setpoint != self.previous_pid_setpoint:
    -                        self.pid_setpoint_status_signal.emit(pid_setpoint)
    -                    
    -                    self.previous_pid_setpoint = pid_setpoint
    -                    
    -                    if pid_enabled != self.previous_pid_enabled_status_signal:
    -                        self.pid_enabled_status_signal.emit(pid_enabled)
    -                        
    -                    self.previous_pid_enabled_status_signal = pid_enabled
    -                    
    -                    if pid_state != self.previous_pid_update_status_signal:
    -                        self.pid_update_status_signal.emit(pid_state)
    -                    
    -                    self.previous_pid_update_status_signal = pid_state
    -                    
    -                    if pid_const != self.previous_pid_constants_status_signal:
    -                        self.pid_constants_status_signal.emit(pid_const)
    -                    
    -                    self.previous_pid_constants_status_signal = pid_const
    -                             
    -            except ZMQError as exc:
    -                raise exc
    -
    -

    Ancestors

    -
      -
    • PyQt5.QtCore.QObject
    • -
    • sip.wrapper
    • -
    • sip.simplewrapper
    • -
    -

    Methods

    -
    -
    -def connect_socket(self) -
    -
    -

    Create a socket and connect to the monitoring port.

    -
    - -Expand source code - -
    def connect_socket(self):
    -    """ Create a socket and connect to the monitoring port.
    -    """
    -    
    -
    -    try:
    -        transport   = PID_CTRL_SETTINGS.PROTOCOL
    -        hostname    = PID_CTRL_SETTINGS.HOSTNAME
    -        
    -        monitoring_port = PID_CTRL_SETTINGS.MONITORING_PORT
    -        monitoring_address = connect_address(transport, hostname, monitoring_port)
    -        
    -        self.monitoring_socket = zmq.Context().socket(zmq.SUB)
    -        self.monitoring_socket.connect(monitoring_address)
    -        self.monitoring_socket.setsockopt_string(zmq.SUBSCRIBE, "")
    -        
    -        self.monitoring_timeout = 0.5
    -        
    -        self.is_socket_connected = True
    -        
    -    except:
    -        self.is_socket_connected = False
    -
    -
    -
    -def pid_constants_status_signal(...) -
    -
    -
    -
    -
    -def pid_enabled_status_signal(...) -
    -
    -
    -
    -
    -def pid_setpoint_status_signal(...) -
    -
    -
    -
    -
    -def pid_timestamp_status_signal(...) -
    -
    -
    -
    -
    -def pid_update_status_signal(...) -
    -
    -
    -
    -
    -def run(self) -
    -
    -

    Keep on checkin whether the Beaglebone state has changed

    -

    If the beaglebone status has changed, update it in the GUI

    -

    Raises

    -
    -
    Exception
    -
    ZMQ Error
    -
    -
    - -Expand source code - -
    @pyqtSlot()
    -def run(self):
    -    """Keep on checkin whether the Beaglebone state has changed
    -    
    -    If the beaglebone status has changed, update it in the GUI
    -    Raises:
    -        Exception: ZMQ Error
    -    """
    -    
    -    self.active = True
    -    while self.is_socket_connected and self.active:
    -        
    -        try:
    -            socket_list, _, exc_list = zmq.select([self.monitoring_socket], [], [], timeout=self.monitoring_timeout)
    -
    -            if self.monitoring_socket in socket_list:
    -                try:
    -                    pickle_string = self.monitoring_socket.recv()
    -                except Exception:
    -                    raise Exception
    -
    -                monitoring_info = pickle.loads(pickle_string)
    -                
    -                pid_enabled     = monitoring_info['Enabled']
    -                pid_setpoint    = monitoring_info['Setpoint']
    -                pid_timestamp   = monitoring_info['Timestamp']
    -                pid_error       = monitoring_info['Error']
    -                pid_isum        = monitoring_info['Isum']
    -                pid_input       = monitoring_info['Input']
    -                pid_output      = monitoring_info['Output']
    -                pid_const       = monitoring_info['PidConst']
    -                pid_state       = [pid_error, pid_isum, pid_input, pid_output]
    -                
    -                if pid_timestamp != self.previous_pid_timestamp_status_signal:
    -                    self.pid_timestamp_status_signal.emit(pid_timestamp)
    -                    
    -                self.previous_pid_timestamp_status_signal = pid_timestamp
    -                
    -                if pid_setpoint != self.previous_pid_setpoint:
    -                    self.pid_setpoint_status_signal.emit(pid_setpoint)
    -                
    -                self.previous_pid_setpoint = pid_setpoint
    -                
    -                if pid_enabled != self.previous_pid_enabled_status_signal:
    -                    self.pid_enabled_status_signal.emit(pid_enabled)
    -                    
    -                self.previous_pid_enabled_status_signal = pid_enabled
    -                
    -                if pid_state != self.previous_pid_update_status_signal:
    -                    self.pid_update_status_signal.emit(pid_state)
    -                
    -                self.previous_pid_update_status_signal = pid_state
    -                
    -                if pid_const != self.previous_pid_constants_status_signal:
    -                    self.pid_constants_status_signal.emit(pid_const)
    -                
    -                self.previous_pid_constants_status_signal = pid_const
    -                         
    -        except ZMQError as exc:
    -            raise exc
    -
    -
    -
    -def start_process(self) -
    -
    -

    Start updated the Beaglebone status

    -
    - -Expand source code - -
    def start_process(self):
    -    """Start updated the Beaglebone status"""
    -    self.run()
    -
    -
    -
    -def stop(self) -
    -
    -

    Stop the monitoring worker. -The monitoring socket is disconnected from the monitoring port and is then closed immediately.

    -
    - -Expand source code - -
    def stop(self):
    -    
    -    """ Stop the monitoring worker.
    -    The monitoring socket is disconnected from the monitoring port and is then closed immediately.
    -    """
    -    
    -    self.monitoring_socket.close()
    -    self.is_socket_connected = False
    -    
    -    self.active = False
    -
    -
    -
    -
    -
    -class pidChannelWidget -(channel_info, parent=None) -
    -
    -

    QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

    -
    - -Expand source code - -
    class pidChannelWidget(QGroupBox):
    -    def __init__(self, channel_info, parent=None):
    -        super().__init__(parent=parent)
    -     
    -        
    -        self.pid = channel_info[0]
    -        self.htr = channel_info[3]
    -        self.ch  = channel_info[4]
    -        self.setObjectName("PID channel {} widget".format(self.ch))
    -        self.setMinimumWidth(380)
    -        sizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
    -        sizePolicy.setHorizontalStretch(0)
    -        sizePolicy.setVerticalStretch(0)
    -        sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth())
    -        self.setSizePolicy(sizePolicy)
    -        self.verticalLayout = QVBoxLayout(self)
    -        self.verticalLayout.setObjectName("verticalLayout")
    -        self.gridLayout = QGridLayout()
    -        self.gridLayout.setContentsMargins(0, -1, 0, -1)
    -        self.gridLayout.setObjectName("gridLayout")
    -        
    -        self.widget = ToggleButton(
    -            name="Turn On\Off PID Channel",
    -            status_tip=f"Heater {self.htr} Channel {self.ch} On\Off",
    -            selected=get_resource(":/icons/switch-on.svg"),
    -            not_selected=get_resource(":/icons/switch-off.svg"),
    -        )
    -        self.widget.setMinimumSize(QSize(35, 35))
    -        self.widget.setObjectName("widget")    
    -        self.gridLayout.addWidget(self.widget, 0, 0)
    -        self.label = QLabel(self)
    -        self.label.setMinimumSize(QSize(0, 0))
    -        self.label.setObjectName("label")
    -        self.label.setText("Heater {} Channel {}".format(int(self.htr + 1), chr(int(self.ch) + 65)))
    -        self.gridLayout.addWidget(self.label, 0, 1, 1, 5)
    -        self.label_2 = QLabel(self)
    -        self.label_2.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
    -        self.label_2.setObjectName("label_2")
    -        self.label_2.setText("Setpoint:")
    -        self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1)
    -        self.label_3 = QLabel(self)
    -        self.label_3.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
    -        self.label_3.setObjectName("label_3")
    -        self.label_3.setText("Input:")
    -        self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1)
    -        self.label_6 = QLabel(self)
    -        self.label_6.setObjectName("label_6")
    -        self.label_6.setText("Err:")
    -        self.gridLayout.addWidget(self.label_6, 2, 2, 1, 1)
    -        self.doubleSpinBox = QDoubleSpinBox(self)
    -        self.doubleSpinBox.setMinimumSize(QSize(90, 0))
    -        self.doubleSpinBox.setProperty("value", 21.0)
    -        self.doubleSpinBox.setObjectName("doubleSpinBox")
    -        self.doubleSpinBox.setMinimum(-250)
    -        self.gridLayout.addWidget(self.doubleSpinBox, 2, 1, 1, 1)
    -        self.label_13 = QLabel(self)
    -        self.label_13.setObjectName("label_13")
    -        self.label_13.setText("Timestamp:")
    -        self.gridLayout.addWidget(self.label_13, 5, 1, 1, 2)
    -        self.label_12 = QLabel(self)
    -        self.label_12.setAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
    -        self.label_12.setObjectName("label_12")
    -        self.label_12.setText("Out:")
    -        self.gridLayout.addWidget(self.label_12, 4, 0, 1, 1)
    -        self.label_7 = QLabel(self)
    -        self.label_7.setObjectName("label_7")
    -        self.label_7.setText("I:")
    -        self.gridLayout.addWidget(self.label_7, 3, 2, 1, 1)
    -        self.label_14 = QLabel(self)
    -        self.label_14.setObjectName("label_14")
    -        self.gridLayout.addWidget(self.label_14, 6, 1, 1, 4)
    -        self.lineEdit_3 = QLineEdit(self)
    -        self.lineEdit_3.setMinimumSize(QSize(90, 0))
    -        self.lineEdit_3.setReadOnly(True)
    -        self.lineEdit_3.setObjectName("lineEdit_3")
    -        self.gridLayout.addWidget(self.lineEdit_3, 3, 3, 1, 1)
    -        self.lineEdit_4 = QLineEdit(self)
    -        self.lineEdit_4.setMinimumSize(QSize(90, 0))
    -        self.lineEdit_4.setReadOnly(True)
    -        self.lineEdit_4.setObjectName("lineEdit_4")
    -        self.gridLayout.addWidget(self.lineEdit_4, 4, 1, 1, 1)
    -        self.lineEdit_2 = QLineEdit(self)
    -        self.lineEdit_2.setMinimumSize(QSize(90, 0))
    -        self.lineEdit_2.setReadOnly(True)
    -        self.lineEdit_2.setObjectName("lineEdit_2")
    -        self.gridLayout.addWidget(self.lineEdit_2, 3, 1, 1, 1)
    -        self.lineEdit = QLineEdit(self)
    -        self.lineEdit.setMinimumSize(QSize(90, 0))
    -        self.lineEdit.setReadOnly(True)
    -        self.lineEdit.setObjectName("lineEdit")
    -        self.gridLayout.addWidget(self.lineEdit, 2, 3, 1, 1)
    -        self.pushButton = QPushButton(self)
    -        self.pushButton.setMaximumSize(QSize(50, 16777215))
    -        self.pushButton.setObjectName("pushButton")
    -        self.pushButton.setText("Set")
    -        self.gridLayout.addWidget(self.pushButton, 2, 4, 1, 1)
    -        self.widget_2 = ToggleButton(
    -            width=10,
    -            height=10,
    -            name="More PID information",
    -            status_tip=f"Show more information about pid channel",
    -            selected=get_resource(":/icons/more.svg"),
    -            not_selected=get_resource(":/icons/arrow-up.svg")
    -        )
    -        self.widget_2.setObjectName("widget_2")
    -        self.gridLayout.addWidget(self.widget_2, 6, 5, 1, 1)
    -        self.horizontalLayout_2 = QHBoxLayout()
    -        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
    -        self.label_4 = QLabel(self)
    -        self.label_4.setObjectName("label_4")
    -        self.label_4.setText("Kp:")
    -        self.horizontalLayout_2.addWidget(self.label_4)
    -        self.label_5 = QLabel(self)
    -        self.label_5.setObjectName("label_5")
    -        self.label_5.setText("0")
    -        self.horizontalLayout_2.addWidget(self.label_5)
    -        self.label_8 = QLabel(self)
    -        self.label_8.setObjectName("label_8")
    -        self.label_8.setText("Ki:")
    -        self.horizontalLayout_2.addWidget(self.label_8)
    -        self.label_9 = QLabel(self)
    -        self.label_9.setObjectName("label_9")
    -        self.label_9.setText("0")
    -        self.horizontalLayout_2.addWidget(self.label_9)
    -        self.label_10 =QLabel(self)
    -        self.label_10.setObjectName("label_10")
    -        self.label_10.setText("Kd:")
    -        self.horizontalLayout_2.addWidget(self.label_10)
    -        self.label_11 = QLabel(self)
    -        self.label_11.setObjectName("label_11")
    -        self.label_11.setText("0")
    -        self.horizontalLayout_2.addWidget(self.label_11)
    -        self.gridLayout.addLayout(self.horizontalLayout_2, 1, 0, 1, 6)
    -        self.verticalLayout.addLayout(self.gridLayout)
    -
    -        self.widget_2.clicked.connect(self.show_more_less)
    -        
    -        self.show_less()
    -        
    -    def show_less(self):
    -        self.lineEdit_3.hide()
    -        self.lineEdit_4.hide()
    -        self.label_4.hide()
    -        self.label_5.hide()
    -        self.label_7.hide()
    -        self.label_8.hide()
    -        self.label_9.hide()
    -        self.label_10.hide()
    -        self.label_11.hide()
    -        self.label_12.hide()
    -    
    -    def show_more(self):
    -        self.lineEdit_3.show()
    -        self.lineEdit_4.show()
    -        self.label_4.show()
    -        self.label_5.show()
    -        self.label_7.show()
    -        self.label_8.show()
    -        self.label_9.show()
    -        self.label_10.show()
    -        self.label_11.show()
    -        self.label_12.show()
    -    
    -    def show_more_less(self):
    -        if not self.widget_2.is_selected():
    -            self.show_more()
    -        else:
    -            self.show_less()
    -
    -

    Ancestors

    -
      -
    • PyQt5.QtWidgets.QGroupBox
    • -
    • PyQt5.QtWidgets.QWidget
    • -
    • PyQt5.QtCore.QObject
    • -
    • sip.wrapper
    • -
    • PyQt5.QtGui.QPaintDevice
    • -
    • sip.simplewrapper
    • -
    -

    Methods

    -
    -
    -def show_less(self) -
    -
    -
    -
    - -Expand source code - -
    def show_less(self):
    -    self.lineEdit_3.hide()
    -    self.lineEdit_4.hide()
    -    self.label_4.hide()
    -    self.label_5.hide()
    -    self.label_7.hide()
    -    self.label_8.hide()
    -    self.label_9.hide()
    -    self.label_10.hide()
    -    self.label_11.hide()
    -    self.label_12.hide()
    -
    -
    -
    -def show_more(self) -
    -
    -
    -
    - -Expand source code - -
    def show_more(self):
    -    self.lineEdit_3.show()
    -    self.lineEdit_4.show()
    -    self.label_4.show()
    -    self.label_5.show()
    -    self.label_7.show()
    -    self.label_8.show()
    -    self.label_9.show()
    -    self.label_10.show()
    -    self.label_11.show()
    -    self.label_12.show()
    -
    -
    -
    -def show_more_less(self) -
    -
    -
    -
    - -Expand source code - -
    def show_more_less(self):
    -    if not self.widget_2.is_selected():
    -        self.show_more()
    -    else:
    -        self.show_less()
    -
    -
    -
    -
    -
    -class pidGroupbox -(pid_name, channels, parent=None) -
    -
    -

    QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

    -
    - -Expand source code - -
    class pidGroupbox(QGroupBox):
    -    def __init__(self, pid_name, channels, parent=None):
    -        super().__init__(parent=parent)
    -        self.setObjectName("{}".format(pid_name))
    -        self.setMaximumWidth(400)
    -        self.verticalLayout = QVBoxLayout(self)
    -        self.verticalLayout.setObjectName("verticalLayout")
    -        self.horizontalLayout = QHBoxLayout()
    -        self.horizontalLayout.setObjectName("horizontalLayout")
    -        self.label = QLabel(self)
    -        self.label.setText(pid_name)
    -        sizePolicy = QSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
    -        sizePolicy.setHorizontalStretch(0)
    -        sizePolicy.setVerticalStretch(0)
    -        sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
    -        self.label.setSizePolicy(sizePolicy)
    -        self.label.setObjectName("label")
    -        self.horizontalLayout.addWidget(self.label)
    -        
    -        self.widget_2 = ToggleButton(
    -            name="Turn On\Off PID Channel",
    -            status_tip=pid_name,
    -            selected=get_resource(":/icons/switch-on.svg"),
    -            not_selected=get_resource(":/icons/switch-off.svg"),
    -        )
    -        
    -        self.doubleSpinBox = QDoubleSpinBox(self)
    -        self.doubleSpinBox.setMinimumSize(QSize(90, 0))
    -        self.doubleSpinBox.setMinimum(-250)
    -        
    -        self.doubleSpinBox.setObjectName("doubleSpinBox")
    -        
    -        self.pushButton = QPushButton('set')
    -        self.pushButton.setMaximumSize(QSize(50, 16777215))
    -        
    -        self.horizontalLayout.addWidget(self.widget_2)
    -        spacerItem =QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
    -        self.horizontalLayout.addItem(spacerItem)
    -        self.horizontalLayout.addWidget(self.doubleSpinBox)
    -        self.horizontalLayout.addWidget(self.pushButton)
    -        
    -        
    -        self.widget = LED()
    -        self.widget.setMinimumSize(QSize(25, 25))
    -        self.widget.setObjectName("widget")
    -        self.horizontalLayout.addWidget(self.widget)
    -        self.verticalLayout.addLayout(self.horizontalLayout)
    -        self.channels = [None] * len(channels)
    -        for idx, channel in enumerate(channels):
    -            if (channel[1] == 0 and channel[2] == 0):
    -                continue
    -            self.channels[idx] = pidChannelWidget(channel)
    -            self.verticalLayout.addWidget(self.channels[idx])
    -        spacerItem_2 =QSpacerItem(40, 20, QSizePolicy.Minimum, QSizePolicy.Expanding)
    -        self.verticalLayout.addItem(spacerItem_2)
    -
    -

    Ancestors

    -
      -
    • PyQt5.QtWidgets.QGroupBox
    • -
    • PyQt5.QtWidgets.QWidget
    • -
    • PyQt5.QtCore.QObject
    • -
    • sip.wrapper
    • -
    • PyQt5.QtGui.QPaintDevice
    • -
    • sip.simplewrapper
    • -
    -
    -
    -class pidMainWindow -(parent=None) -
    -
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    -
    - -Expand source code - -
    class pidMainWindow(QMainWindow, Observable):
    -    def __init__(self, parent=None):
    -        super().__init__(parent=parent)
    -        self.setWindowTitle("Beaglebone PID Controller")
    -        self.setGeometry(600, 100, 1250, 670)
    -        self.setMaximumWidth(1250)
    -        
    -        setup = get_setup()
    -        self.pid_configuration = setup.gse.spid.configuration.heaters
    -        self.pid_configuration['MaRi'] = self.pid_configuration['TRP2'] + self.pid_configuration['TRP3'] + self.pid_configuration['TRP4']
    -        del self.pid_configuration['TRP2'], self.pid_configuration['TRP3'], self.pid_configuration['TRP4']
    -
    -        self.pid_monitoring_thread = QThread(self)
    -        self.pid_monitoring_worker = PidMonitoringWorker()
    -        self.pid_monitoring_worker.moveToThread(self.pid_monitoring_thread)
    -        
    -        self.pid_monitoring_worker.pid_enabled_status_signal.connect(self.on_pid_enabled_status_signal)
    -        self.pid_monitoring_worker.pid_setpoint_status_signal.connect(self.on_pid_setpoint_status_signal)
    -        self.pid_monitoring_worker.pid_timestamp_status_signal.connect(self.on_pid_timestamp_status_signal)
    -        self.pid_monitoring_worker.pid_update_status_signal.connect(self.on_pid_update_status_signal)
    -        self.pid_monitoring_worker.pid_constants_status_signal.connect(self.on_pid_constant_status_signal)
    -        
    -        self.pid_monitoring_thread.started.connect(self.pid_monitoring_worker.start_process)
    -        self.pid_monitoring_thread.start()
    -        
    -        self.initUI()
    -
    -    def on_pid_enabled_status_signal(self, monitoring_info:dict):
    -        monitoring_info = list(monitoring_info.values())
    -        
    -        for idx, (_, configuration) in enumerate(self.pid_configuration.items()):
    -            state_lin = len(self.pid_channels[idx].channels)
    -            pid_state = 0
    -            for ch_idx, ch in enumerate(configuration):
    -                if (ch[1] == 0 and ch[2] == 0):
    -                    state_lin -= 1
    -                    continue
    -                self.pid_channels[idx].channels[ch_idx].widget.set_selected(bool(monitoring_info[ch[0]]))
    -                ch_enabled = self.pid_channels[idx].channels[ch_idx].widget.is_selected()
    -                pid_state += 1 if int(ch_enabled) else 0
    -            if pid_state == state_lin:
    -                self.pid_channels[idx].widget.set_color(1)
    -                self.pid_channels[idx].widget_2.set_selected(True)
    -            elif  0 < pid_state < state_lin:
    -                self.pid_channels[idx].widget_2.set_selected(False)
    -                self.pid_channels[idx].widget.set_color(2)
    -            else:
    -                self.pid_channels[idx].widget_2.set_selected(False)
    -                self.pid_channels[idx].widget.set_color(0)
    -    
    -    def on_pid_constant_status_signal(self, monitoring_info):
    -        for idx, (_, configuration) in enumerate(self.pid_configuration.items()):
    -            for ch_idx, ch in enumerate(configuration):
    -                if (ch[1] == 0 and ch[2] == 0):
    -                    continue                
    -                self.pid_channels[idx].channels[ch_idx].label_5.setText(f"{monitoring_info[idx][0]:.2e}")
    -                self.pid_channels[idx].channels[ch_idx].label_9.setText(f"{monitoring_info[idx][1]:.2e}")  
    -                self.pid_channels[idx].channels[ch_idx].label_11.setText(f"{monitoring_info[idx][2]:.2e}")  
    -    
    -    def on_pid_setpoint_status_signal(self, monitoring_info):
    -        for idx, (_, configuration) in enumerate(self.pid_configuration.items()):
    -            setpoint = []
    -            for ch_idx, ch in enumerate(configuration):
    -                if (ch[1] == 0 and ch[2] == 0):
    -                    continue                
    -                self.pid_channels[idx].channels[ch_idx].doubleSpinBox.setValue(float(monitoring_info[ch[0]]))
    -                setpoint.append(float(monitoring_info[ch[0]]))
    -            self.pid_channels[idx].doubleSpinBox.setValue(float(np.amax(setpoint)))
    -                
    -    def on_pid_timestamp_status_signal(self, monitoring_info):
    -        for idx, (_, configuration) in enumerate(self.pid_configuration.items()):
    -            for ch_idx, ch in enumerate(configuration):
    -                if (ch[1] == 0 and ch[2] == 0):
    -                    continue
    -                self.pid_channels[idx].channels[ch_idx].label_14.setText(str(datetime.fromtimestamp(monitoring_info[int(ch[0])])))
    -    
    -    def on_pid_update_status_signal(self, monitoring_info):
    -        channels = 0
    -        for idx, (pid, configuration) in enumerate(self.pid_configuration.items()):
    -            for ch_idx, ch in enumerate(configuration):
    -                if (ch[1] == 0 and ch[2] == 0):
    -                    continue
    -                self.pid_channels[idx].channels[ch_idx].lineEdit.setText(f"{monitoring_info[0][channels]:.2e}")
    -                self.pid_channels[idx].channels[ch_idx].lineEdit_3.setText(f"{monitoring_info[1][channels]:.2e}")
    -                self.pid_channels[idx].channels[ch_idx].lineEdit_2.setText(f"{monitoring_info[2][channels]:.2e}")
    -                self.pid_channels[idx].channels[ch_idx].lineEdit_4.setText(f"{monitoring_info[3][channels]:.2e}")
    -                channels += 1
    -
    -    def initUI(self):
    -        scroll_frame = QScrollArea()
    -        scroll_frame.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
    -        scroll_frame.setWidgetResizable(True)
    -        app_frame = QFrame()
    -        grid_layout = QGridLayout()
    -        self.pid_channels = [None] * len(self.pid_configuration)
    -        for idx, (pid, configuration) in enumerate(self.pid_configuration.items()):
    -            configuration.sort( key = lambda configuration: (configuration[3], configuration[4]))
    -            self.pid_channels[idx] = pidGroupbox(pid, configuration)
    -            
    -            self.pid_channels[idx].pushButton.clicked.connect(self.set_all_pid_setpoint)
    -            self.pid_channels[idx].widget_2.clicked.connect(self.set_all_pid_control)
    -            
    -            grid_layout.addWidget(self.pid_channels[idx], int(idx / 3), idx % 3)
    -            for ch_idx, ch in enumerate(configuration):
    -                if (ch[1] == 0 and ch[2] == 0):
    -                    continue
    -                self.pid_channels[idx].channels[ch_idx].widget.clicked.connect(self.set_pid_control)
    -                self.pid_channels[idx].channels[ch_idx].pushButton.clicked.connect(self.set_pid_setpoint)
    -        app_frame.setLayout(grid_layout)
    -        scroll_frame.setWidget(app_frame)
    -        self.setCentralWidget(scroll_frame)
    -
    -    def set_all_pid_control(self):
    -        sender = self.sender()
    -        name = sender.parent().objectName()
    -        new_state = sender.is_selected()
    -        sender.set_selected(on=new_state)
    -        for channel in self.pid_configuration[name]:
    -            try:
    -                self.actionObservers({"set_pid_control" : [channel[0], bool(new_state)]})
    -            except Exception as e:
    -                warning_popup("set_pid_control : Channel {} {}".format(pid, "Enabled" 
    -                                                                        if new_state else "Disabled"), e)
    -    def set_pid_control(self):
    -        sender = self.sender()
    -        state = sender.is_selected()
    -        new_state = state
    -        sender.set_selected(on=new_state)
    -        pid = sender.parent().pid
    -        htr = sender.parent().htr
    -        ch  = sender.parent().ch
    -
    -        try:
    -            self.actionObservers({"set_pid_control" : [pid, bool(new_state)]})
    -        except Exception as e:
    -            warning_popup("set_pid_control : Channel {} {}".format(pid, "Enabled" 
    -                                                                   if new_state else "Disabled"), e)
    -    
    -    def set_all_pid_setpoint(self):
    -        sender = self.sender()
    -        setpoint = float(sender.parent().doubleSpinBox.value())
    -        name = sender.parent().objectName()
    -        
    -        for channel in self.pid_configuration[name]:
    -            try:
    -                self.actionObservers({"set_pid_setpoint" : [channel[0], float(setpoint)]})
    -            except Exception as e:
    -                 warning_popup("set_pid_control : Channel {} {}".format(pid, "Enabled" 
    -                                                                    if new_state else "Disabled"), e)
    -    
    -    def set_pid_setpoint(self):
    -        sender = self.sender()
    -        setpoint = float(sender.parent().doubleSpinBox.value())
    -        pid = sender.parent().pid
    -        htr = sender.parent().htr
    -        ch  = sender.parent().ch
    -
    -        try:
    -            self.actionObservers({"set_pid_setpoint" : [pid, float(setpoint)]})
    -        except Exception as e:
    -            warning_popup("set_pid_setpoint : PID Channel {} to {} C".format(pid, setpoint), e)      
    -              
    -    def closeEvent(self, close_Event: QCloseEvent) -> None:
    -        self.pid_monitoring_thread.quit()
    -        self.pid_monitoring_worker.stop()
    -
    -

    Ancestors

    -
      -
    • PyQt5.QtWidgets.QMainWindow
    • -
    • PyQt5.QtWidgets.QWidget
    • -
    • PyQt5.QtCore.QObject
    • -
    • sip.wrapper
    • -
    • PyQt5.QtGui.QPaintDevice
    • -
    • sip.simplewrapper
    • -
    • Observable
    • -
    -

    Methods

    -
    -
    -def closeEvent(self, close_Event: PyQt5.QtGui.QCloseEvent) ‑> None -
    -
    -

    closeEvent(self, QCloseEvent)

    -
    - -Expand source code - -
    def closeEvent(self, close_Event: QCloseEvent) -> None:
    -    self.pid_monitoring_thread.quit()
    -    self.pid_monitoring_worker.stop()
    -
    -
    -
    -def initUI(self) -
    -
    -
    -
    - -Expand source code - -
    def initUI(self):
    -    scroll_frame = QScrollArea()
    -    scroll_frame.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
    -    scroll_frame.setWidgetResizable(True)
    -    app_frame = QFrame()
    -    grid_layout = QGridLayout()
    -    self.pid_channels = [None] * len(self.pid_configuration)
    -    for idx, (pid, configuration) in enumerate(self.pid_configuration.items()):
    -        configuration.sort( key = lambda configuration: (configuration[3], configuration[4]))
    -        self.pid_channels[idx] = pidGroupbox(pid, configuration)
    -        
    -        self.pid_channels[idx].pushButton.clicked.connect(self.set_all_pid_setpoint)
    -        self.pid_channels[idx].widget_2.clicked.connect(self.set_all_pid_control)
    -        
    -        grid_layout.addWidget(self.pid_channels[idx], int(idx / 3), idx % 3)
    -        for ch_idx, ch in enumerate(configuration):
    -            if (ch[1] == 0 and ch[2] == 0):
    -                continue
    -            self.pid_channels[idx].channels[ch_idx].widget.clicked.connect(self.set_pid_control)
    -            self.pid_channels[idx].channels[ch_idx].pushButton.clicked.connect(self.set_pid_setpoint)
    -    app_frame.setLayout(grid_layout)
    -    scroll_frame.setWidget(app_frame)
    -    self.setCentralWidget(scroll_frame)
    -
    -
    -
    -def on_pid_constant_status_signal(self, monitoring_info) -
    -
    -
    -
    - -Expand source code - -
    def on_pid_constant_status_signal(self, monitoring_info):
    -    for idx, (_, configuration) in enumerate(self.pid_configuration.items()):
    -        for ch_idx, ch in enumerate(configuration):
    -            if (ch[1] == 0 and ch[2] == 0):
    -                continue                
    -            self.pid_channels[idx].channels[ch_idx].label_5.setText(f"{monitoring_info[idx][0]:.2e}")
    -            self.pid_channels[idx].channels[ch_idx].label_9.setText(f"{monitoring_info[idx][1]:.2e}")  
    -            self.pid_channels[idx].channels[ch_idx].label_11.setText(f"{monitoring_info[idx][2]:.2e}")  
    -
    -
    -
    -def on_pid_enabled_status_signal(self, monitoring_info: dict) -
    -
    -
    -
    - -Expand source code - -
    def on_pid_enabled_status_signal(self, monitoring_info:dict):
    -    monitoring_info = list(monitoring_info.values())
    -    
    -    for idx, (_, configuration) in enumerate(self.pid_configuration.items()):
    -        state_lin = len(self.pid_channels[idx].channels)
    -        pid_state = 0
    -        for ch_idx, ch in enumerate(configuration):
    -            if (ch[1] == 0 and ch[2] == 0):
    -                state_lin -= 1
    -                continue
    -            self.pid_channels[idx].channels[ch_idx].widget.set_selected(bool(monitoring_info[ch[0]]))
    -            ch_enabled = self.pid_channels[idx].channels[ch_idx].widget.is_selected()
    -            pid_state += 1 if int(ch_enabled) else 0
    -        if pid_state == state_lin:
    -            self.pid_channels[idx].widget.set_color(1)
    -            self.pid_channels[idx].widget_2.set_selected(True)
    -        elif  0 < pid_state < state_lin:
    -            self.pid_channels[idx].widget_2.set_selected(False)
    -            self.pid_channels[idx].widget.set_color(2)
    -        else:
    -            self.pid_channels[idx].widget_2.set_selected(False)
    -            self.pid_channels[idx].widget.set_color(0)
    -
    -
    -
    -def on_pid_setpoint_status_signal(self, monitoring_info) -
    -
    -
    -
    - -Expand source code - -
    def on_pid_setpoint_status_signal(self, monitoring_info):
    -    for idx, (_, configuration) in enumerate(self.pid_configuration.items()):
    -        setpoint = []
    -        for ch_idx, ch in enumerate(configuration):
    -            if (ch[1] == 0 and ch[2] == 0):
    -                continue                
    -            self.pid_channels[idx].channels[ch_idx].doubleSpinBox.setValue(float(monitoring_info[ch[0]]))
    -            setpoint.append(float(monitoring_info[ch[0]]))
    -        self.pid_channels[idx].doubleSpinBox.setValue(float(np.amax(setpoint)))
    -
    -
    -
    -def on_pid_timestamp_status_signal(self, monitoring_info) -
    -
    -
    -
    - -Expand source code - -
    def on_pid_timestamp_status_signal(self, monitoring_info):
    -    for idx, (_, configuration) in enumerate(self.pid_configuration.items()):
    -        for ch_idx, ch in enumerate(configuration):
    -            if (ch[1] == 0 and ch[2] == 0):
    -                continue
    -            self.pid_channels[idx].channels[ch_idx].label_14.setText(str(datetime.fromtimestamp(monitoring_info[int(ch[0])])))
    -
    -
    -
    -def on_pid_update_status_signal(self, monitoring_info) -
    -
    -
    -
    - -Expand source code - -
    def on_pid_update_status_signal(self, monitoring_info):
    -    channels = 0
    -    for idx, (pid, configuration) in enumerate(self.pid_configuration.items()):
    -        for ch_idx, ch in enumerate(configuration):
    -            if (ch[1] == 0 and ch[2] == 0):
    -                continue
    -            self.pid_channels[idx].channels[ch_idx].lineEdit.setText(f"{monitoring_info[0][channels]:.2e}")
    -            self.pid_channels[idx].channels[ch_idx].lineEdit_3.setText(f"{monitoring_info[1][channels]:.2e}")
    -            self.pid_channels[idx].channels[ch_idx].lineEdit_2.setText(f"{monitoring_info[2][channels]:.2e}")
    -            self.pid_channels[idx].channels[ch_idx].lineEdit_4.setText(f"{monitoring_info[3][channels]:.2e}")
    -            channels += 1
    -
    -
    -
    -def set_all_pid_control(self) -
    -
    -
    -
    - -Expand source code - -
    def set_all_pid_control(self):
    -    sender = self.sender()
    -    name = sender.parent().objectName()
    -    new_state = sender.is_selected()
    -    sender.set_selected(on=new_state)
    -    for channel in self.pid_configuration[name]:
    -        try:
    -            self.actionObservers({"set_pid_control" : [channel[0], bool(new_state)]})
    -        except Exception as e:
    -            warning_popup("set_pid_control : Channel {} {}".format(pid, "Enabled" 
    -                                                                    if new_state else "Disabled"), e)
    -
    -
    -
    -def set_all_pid_setpoint(self) -
    -
    -
    -
    - -Expand source code - -
    def set_all_pid_setpoint(self):
    -    sender = self.sender()
    -    setpoint = float(sender.parent().doubleSpinBox.value())
    -    name = sender.parent().objectName()
    -    
    -    for channel in self.pid_configuration[name]:
    -        try:
    -            self.actionObservers({"set_pid_setpoint" : [channel[0], float(setpoint)]})
    -        except Exception as e:
    -             warning_popup("set_pid_control : Channel {} {}".format(pid, "Enabled" 
    -                                                                if new_state else "Disabled"), e)
    -
    -
    -
    -def set_pid_control(self) -
    -
    -
    -
    - -Expand source code - -
    def set_pid_control(self):
    -    sender = self.sender()
    -    state = sender.is_selected()
    -    new_state = state
    -    sender.set_selected(on=new_state)
    -    pid = sender.parent().pid
    -    htr = sender.parent().htr
    -    ch  = sender.parent().ch
    -
    -    try:
    -        self.actionObservers({"set_pid_control" : [pid, bool(new_state)]})
    -    except Exception as e:
    -        warning_popup("set_pid_control : Channel {} {}".format(pid, "Enabled" 
    -                                                               if new_state else "Disabled"), e)
    -
    -
    -
    -def set_pid_setpoint(self) -
    -
    -
    -
    - -Expand source code - -
    def set_pid_setpoint(self):
    -    sender = self.sender()
    -    setpoint = float(sender.parent().doubleSpinBox.value())
    -    pid = sender.parent().pid
    -    htr = sender.parent().htr
    -    ch  = sender.parent().ch
    -
    -    try:
    -        self.actionObservers({"set_pid_setpoint" : [pid, float(setpoint)]})
    -    except Exception as e:
    -        warning_popup("set_pid_setpoint : PID Channel {} to {} C".format(pid, setpoint), e)      
    -
    -
    -
    -
    -
    -
    -
    - -
    - - - \ No newline at end of file diff --git a/docs/api/egse/tempcontrol/srs/ptc10.html b/docs/api/egse/tempcontrol/srs/ptc10.html index 20b0413..8c8da1d 100644 --- a/docs/api/egse/tempcontrol/srs/ptc10.html +++ b/docs/api/egse/tempcontrol/srs/ptc10.html @@ -34,23 +34,22 @@

    Module egse.tempcontrol.srs.ptc10

    SRS PTC10 controller that will be used in the IAS TVAC setup. """ import logging +from datetime import datetime from typing import List +from time import sleep, strftime, gmtime from egse.decorators import dynamic_interface from egse.device import DeviceInterface -from egse.tempcontrol.srs.ptc10_devif import ptc10Error -from egse.tempcontrol.srs.ptc10_devif import ptc10TelnetInterface +from egse.mixin import add_lf +from egse.mixin import dynamic_command from egse.proxy import Proxy from egse.settings import Settings -from egse.zmq_ser import connect_address -from egse.mixin import dynamic_command -from egse.mixin import add_lf -from time import sleep, strftime, gmtime -from egse.confman import ConfigurationManagerProxy +from egse.setup import load_setup from egse.system import format_datetime -from datetime import datetime - +from egse.tempcontrol.srs.ptc10_devif import ptc10Error +from egse.tempcontrol.srs.ptc10_devif import ptc10TelnetInterface +from egse.zmq_ser import connect_address LOGGER = logging.getLogger(__name__) @@ -58,13 +57,6 @@

    Module egse.tempcontrol.srs.ptc10

    PTC10_SETTINGS = Settings.load("SRS PTC10 Controller") DEVICE_SETTINGS = Settings.load(filename="ptc10.yaml") -def load_setup_from_configuration_manager(): - """Loads a Setup YAML file from the Configuration Manager.""" - with ConfigurationManagerProxy() as cm: - setup = cm.get_setup() - - return setup - class ptc10Interface(DeviceInterface): """ @@ -447,7 +439,7 @@

    Module egse.tempcontrol.srs.ptc10

    self.setpoint_var = None self.PID = None self.limits = None - setup = load_setup_from_configuration_manager() + setup = load_setup() self.T_min_NOP = setup.gse.PTC10.T_min_NOP # TRP2, TRP3 and TRP4 min NOP (°C) super().__init__() @@ -874,11 +866,6 @@

    Module egse.tempcontrol.srs.ptc10

    if i in list_error[:4]: self.disable_heater(sensor_names.index(i) + 1) - def get_time(self) -> str: - smhdmy = self.temp.trans("Systemtime.smh?") - timestamp = format_datetime(datetime.strptime(smhdmy + " +0000", '%S %M %H %d %m %Y %z')) - return timestamp - def update_time(self): """Updates the PTC10 date and time in UTC from the server's time which is ntp synchronised.""" self.temp.write("systemtime.dmy " + strftime("%d/%m/%Y", gmtime())) @@ -889,12 +876,6 @@

    Module egse.tempcontrol.srs.ptc10

    timestamp = format_datetime(datetime.strptime(smhdmy + " +0000", '%S %M %H %d %m %Y %z')) return timestamp - - def update_time(self): - """Updates the PTC10 date and time in UTC from the server's time which is ntp synchronised.""" - self.temp.write("systemtime.dmy " + strftime("%d/%m/%Y", gmtime())) - self.temp.write("systemtime.hms " + strftime("%H:%M:%S", gmtime())) - class ptc10Proxy(Proxy, ptc10Interface): """The ptc10Proxy class is used to connect to the control server and send commands to the SRS PTC10 device remotely.""" @@ -921,26 +902,6 @@

    Module egse.tempcontrol.srs.ptc10

    -

    Functions

    -
    -
    -def load_setup_from_configuration_manager() -
    -
    -

    Loads a Setup YAML file from the Configuration Manager.

    -
    - -Expand source code - -
    def load_setup_from_configuration_manager():
    -    """Loads a Setup YAML file from the Configuration Manager."""
    -    with ConfigurationManagerProxy() as cm:
    -        setup = cm.get_setup()
    -
    -    return setup
    -
    -
    -

    Classes

    @@ -964,7 +925,7 @@

    Classes

    self.setpoint_var = None self.PID = None self.limits = None - setup = load_setup_from_configuration_manager() + setup = load_setup() self.T_min_NOP = setup.gse.PTC10.T_min_NOP # TRP2, TRP3 and TRP4 min NOP (°C) super().__init__() @@ -1391,11 +1352,6 @@

    Classes

    if i in list_error[:4]: self.disable_heater(sensor_names.index(i) + 1) - def get_time(self) -> str: - smhdmy = self.temp.trans("Systemtime.smh?") - timestamp = format_datetime(datetime.strptime(smhdmy + " +0000", '%S %M %H %d %m %Y %z')) - return timestamp - def update_time(self): """Updates the PTC10 date and time in UTC from the server's time which is ntp synchronised.""" self.temp.write("systemtime.dmy " + strftime("%d/%m/%Y", gmtime())) @@ -1404,13 +1360,7 @@

    Classes

    def get_time(self) -> str: smhdmy = self.temp.trans("Systemtime.smh?") timestamp = format_datetime(datetime.strptime(smhdmy + " +0000", '%S %M %H %d %m %Y %z')) - return timestamp - - - def update_time(self): - """Updates the PTC10 date and time in UTC from the server's time which is ntp synchronised.""" - self.temp.write("systemtime.dmy " + strftime("%d/%m/%Y", gmtime())) - self.temp.write("systemtime.hms " + strftime("%H:%M:%S", gmtime()))
    + return timestamp

    Ancestors

  • -
  • Functions

    - -
  • Classes

    • diff --git a/docs/api/egse/tempcontrol/srs/ptc10_cs.html b/docs/api/egse/tempcontrol/srs/ptc10_cs.html index 772c95c..b5836d6 100644 --- a/docs/api/egse/tempcontrol/srs/ptc10_cs.html +++ b/docs/api/egse/tempcontrol/srs/ptc10_cs.html @@ -99,7 +99,7 @@

      Module egse.tempcontrol.srs.ptc10_cs

      try: return CTRL_SETTINGS.STORAGE_MNEMONIC except AttributeError: - return "PTC10" + return "DAS-PTC10-EMPTY" @click.group() @@ -253,7 +253,7 @@

      Classes

      try: return CTRL_SETTINGS.STORAGE_MNEMONIC except AttributeError: - return "PTC10"
      + return "DAS-PTC10-EMPTY"

      Ancestors

        @@ -326,7 +326,7 @@

        Methods

        try: return CTRL_SETTINGS.STORAGE_MNEMONIC except AttributeError: - return "PTC10"
        + return "DAS-PTC10-EMPTY"
  • @@ -334,10 +334,15 @@

    Inherited members

    • ControlServer:
    • diff --git a/docs/api/egse/tempcontrol/srs/ptc10_devif.html b/docs/api/egse/tempcontrol/srs/ptc10_devif.html index 664065e..8ba8c3f 100644 --- a/docs/api/egse/tempcontrol/srs/ptc10_devif.html +++ b/docs/api/egse/tempcontrol/srs/ptc10_devif.html @@ -35,18 +35,16 @@

      Module egse.tempcontrol.srs.ptc10_devif

      """ import logging -import time -import datetime -from typing import List - from telnetlib import Telnet +from typing import List +import time -from egse.settings import Settings -from egse.exceptions import DeviceNotFoundError -from egse.device import DeviceConnectionError -from egse.device import DeviceConnectionInterface, DeviceTransport, DeviceTimeoutError from egse.command import ClientServerCommand +from egse.device import DeviceConnectionError +from egse.device import DeviceConnectionInterface, DeviceTransport +from egse.exceptions import DeviceNotFoundError +from egse.settings import Settings logger = logging.getLogger(__name__) diff --git a/docs/api/egse/tempcontrol/srs/ptc10_protocol.html b/docs/api/egse/tempcontrol/srs/ptc10_protocol.html index afe6362..2071c87 100644 --- a/docs/api/egse/tempcontrol/srs/ptc10_protocol.html +++ b/docs/api/egse/tempcontrol/srs/ptc10_protocol.html @@ -27,14 +27,11 @@

      Module egse.tempcontrol.srs.ptc10_protocol

      Expand source code
      from egse.control import ControlServer
      -from egse.metrics import define_metrics
      -from egse.synoptics import SynopticsManagerProxy
      -from egse.tempcontrol.srs.ptc10 import ptc10Controller, ptc10Simulator, ptc10Interface
      -from egse.tempcontrol.srs.ptc10_devif import ptc10Command
      -
       from egse.protocol import CommandProtocol
       from egse.settings import Settings
      -from egse.system import format_datetime
      +# from egse.metrics import define_metrics
      +from egse.tempcontrol.srs.ptc10 import ptc10Controller, ptc10Simulator, ptc10Interface
      +from egse.tempcontrol.srs.ptc10_devif import ptc10Command
       from egse.zmq_ser import bind_address
       
       COMMAND_SETTINGS = Settings.load(filename="ptc10.yaml")
      @@ -56,9 +53,6 @@ 

      Module egse.tempcontrol.srs.ptc10_protocol

      self.build_device_method_lookup_table(self.temp) - self.synoptics = SynopticsManagerProxy() - self.metrics = define_metrics("DAS-PTC10") - def get_bind_address(self): return bind_address( self.control_server.get_communication_protocol(), @@ -115,9 +109,6 @@

      Classes

      self.build_device_method_lookup_table(self.temp) - self.synoptics = SynopticsManagerProxy() - self.metrics = define_metrics("DAS-PTC10") - def get_bind_address(self): return bind_address( self.control_server.get_communication_protocol(), diff --git a/docs/api/egse/tempcontrol/srs/ptc10_ui.html b/docs/api/egse/tempcontrol/srs/ptc10_ui.html index 73a74c1..f7fa57f 100644 --- a/docs/api/egse/tempcontrol/srs/ptc10_ui.html +++ b/docs/api/egse/tempcontrol/srs/ptc10_ui.html @@ -1042,7 +1042,7 @@

      Classes

      class ConfigureModes
      -

      QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      +

      QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      Expand source code @@ -1075,7 +1075,7 @@

      Ancestors

      class OutputWidget
      -

      QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      +

      QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      Expand source code @@ -1121,7 +1121,7 @@

      Ancestors

      class PIDSettings
      -

      QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      +

      QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      Expand source code @@ -1243,7 +1243,7 @@

      Methods

      class SystemStatusWidget
      -

      QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      +

      QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      Expand source code @@ -1320,7 +1320,7 @@

      Methods

      class createTRPWidget
      -

      QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      +

      QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      Expand source code @@ -1384,7 +1384,7 @@

      Ancestors

      class manualSetting
      -

      QWidget(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      +

      QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      Expand source code @@ -1815,7 +1815,7 @@

      Methods

      class ptc10UIView
      -

      QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      +

      QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

      Expand source code diff --git a/docs/api/egse/ups/apc/apc.html b/docs/api/egse/ups/apc/apc.html index 1a84f48..67bd64e 100644 --- a/docs/api/egse/ups/apc/apc.html +++ b/docs/api/egse/ups/apc/apc.html @@ -27,20 +27,46 @@

      Module egse.ups.apc.apc

      Expand source code
      import logging
      +import socket
      +from collections import OrderedDict
       
       from egse.decorators import dynamic_interface
       from egse.device import DeviceInterface
       from egse.proxy import Proxy
       from egse.settings import Settings
      -from egse.ups.apc.apc_devif import APCError
      -from egse.ups.apc.apc_devif import APCDeviceInterface
       from egse.zmq_ser import connect_address
      +from egse.command import ClientServerCommand
       
       logger = logging.getLogger(__name__)
       
       CTRL_SETTINGS = Settings.load("APC Control Server")
       DEVICE_SETTINGS = Settings.load(filename='apc.yaml')
       
      +CMD_STATUS = "\x00\x06status".encode()
      +EOF = "  \n\x00\x00"
      +SEP = ":"
      +BUFFER_SIZE = 1024
      +ALL_UNITS = (
      +    "Minutes",
      +    "Seconds",
      +    "Percent",
      +    "Volts",
      +    "Watts",
      +    "Amps",
      +    "Hz",
      +    "C",
      +    "VA",
      +    "Percent Load Capacity"
      +)
      +
      +class APCError(Exception):
      +    pass
      +
      +
      +class APCCommand(ClientServerCommand):
      +    def get_cmd_string(self, *args, **kwargs):
      +        out = super().get_cmd_string(*args, **kwargs)
      +        return out + '\n'
       
       class APCInterface(DeviceInterface):
           """ APC base class."""
      @@ -98,43 +124,138 @@ 

      Module egse.ups.apc.apc

      def __init__(self): super().__init__() - - logger.debug('Initalizing APC Controller') - - try: - self.apc = APCDeviceInterface() - self.apc.connect() - except APCError as exc: - logger.warning(f"APCError caught: Couldn't establish connectin ({exc})") - raise APCError( - "Couldn't establish a connection with APC controller." - ) from exc + self._is_connected = True def is_simulator(self): return False def is_connected(self): - return self.apc.is_connected() + return self._is_connected def connect(self): - if not self.apc.is_connected(): - self.apc.connect() + self._is_connected = True def disconnect(self): - self.apc.disconnect() + self._is_connected = False + def reconnect(self): - self.apc.reconnect() + self._is_connected = True - def get_bcharge(self): - return self.apc.get_bcharge() + def get_linev(self): + dct = self._parse(self._get(), True) + return float(dct['LINEV']) + + def get_loadpct(self): + dct = self._parse(self._get(), True) + return float(dct['LOADPCT']) - def get_onbatt(self): - return self.apc.get_onbatt() + def get_bcharge(self): + dct = self._parse(self._get(), True) + return float(dct['BCHARGE']) def get_timeleft(self): - return self.apc.get_timeleft() + dct = self._parse(self._get(), True) + return float(dct['TIMELEFT']) + def get_onbatt(self): + dct = self._parse(self._get(), True) + return 'ONBATT' in dct['STATUS'] + + def get_mbattchg(self): + dct = self._parse(self._get(), True) + return float(dct['MBATTCHG']) + + def get_mintimel(self): + dct = self._parse(self._get(), True) + return float(dct['MINTIMEL']) + + def get_maxtime(self): + dct = self._parse(self._get(), True) + return float(dct['MAXTIME']) + + def get_maxlinev(self): + dct = self._parse(self._get(), True) + return float(dct['MAXLINEV']) + + def get_minlinev(self): + dct = self._parse(self._get(), True) + return float(dct['MINLINEV']) + + def get_outputv(self): + dct = self._parse(self._get(), True) + return float(dct['OUTPUTV']) + + def get_dlowbatt(self): + dct = self._parse(self._get(), True) + return float(dct['DLOWBATT']) + + def get_lotrans(self): + dct = self._parse(self._get(), True) + return float(dct['LOTRANS']) + + def get_hitrans(self): + dct = self._parse(self._get(), True) + return float(dct['HITRANS']) + + def get_itemp(self): + dct = self._parse(self._get(), True) + return float(dct['ITEMP']) + + def get_alarmdel(self): + dct = self._parse(self._get(), True) + return float(dct['ALARMDEL']) + + def get_status_dict(self): + return self._parse(self._get(), True) + + def _get(self, host="localhost", port=3551, timeout=30): + """ + Connect to the APCUPSd NIS and request its status. + """ + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + sock.connect((host, port)) + sock.send(CMD_STATUS) + buffr = "" + while not buffr.endswith(EOF): + buffr += sock.recv(BUFFER_SIZE).decode() + sock.close() + return buffr + + + def _split(self, raw_status): + """ + Split the output from get_status() into lines, removing the length and + newline chars. + """ + # Remove the EOF string, split status on the line endings (\x00), strip the + # length byte and newline chars off the beginning and end respectively. + return [x[1:-1] for x in raw_status[:-len(EOF)].split("\x00") if x] + + + def _parse(self, raw_status, strip_units=False): + """ + Split the output from get_status() into lines, clean it up and return it as + an OrderedDict. + """ + lines = self._split(raw_status) + if strip_units: + lines = self._strip_units_from_lines(lines) + # Split each line on the SEP character, strip extraneous whitespace and + # create an OrderedDict out of the keys/values. + return OrderedDict([[x.strip() for x in x.split(SEP, 1)] for x in lines]) + + + def _strip_units_from_lines(self, lines): + """ + Removes all units from the ends of the lines. + """ + for line in lines: + for unit in ALL_UNITS: + if line.endswith(" %s" % unit): + line = line[:-1-len(unit)] + yield line class APCProxy(Proxy, APCInterface): def __init__(self, protocol=CTRL_SETTINGS.PROTOCOL, @@ -152,6 +273,58 @@

      Module egse.ups.apc.apc

      Classes

      +
      +class APCCommand +(name, cmd, response=None, wait=None, check=None, description=None, device_method=None) +
      +
      +

      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.

      +
      + +Expand source code + +
      class APCCommand(ClientServerCommand):
      +    def get_cmd_string(self, *args, **kwargs):
      +        out = super().get_cmd_string(*args, **kwargs)
      +        return out + '\n'
      +
      +

      Ancestors

      + +

      Methods

      +
      +
      +def get_cmd_string(self, *args, **kwargs) +
      +
      +
      +
      + +Expand source code + +
      def get_cmd_string(self, *args, **kwargs):
      +    out = super().get_cmd_string(*args, **kwargs)
      +    return out + '\n'
      +
      +
      +
      +

      Inherited members

      + +
      class APCController
      @@ -165,42 +338,138 @@

      Classes

      def __init__(self): super().__init__() - - logger.debug('Initalizing APC Controller') - - try: - self.apc = APCDeviceInterface() - self.apc.connect() - except APCError as exc: - logger.warning(f"APCError caught: Couldn't establish connectin ({exc})") - raise APCError( - "Couldn't establish a connection with APC controller." - ) from exc + self._is_connected = True def is_simulator(self): return False def is_connected(self): - return self.apc.is_connected() + return self._is_connected def connect(self): - if not self.apc.is_connected(): - self.apc.connect() + self._is_connected = True def disconnect(self): - self.apc.disconnect() + self._is_connected = False + def reconnect(self): - self.apc.reconnect() + self._is_connected = True - def get_bcharge(self): - return self.apc.get_bcharge() + def get_linev(self): + dct = self._parse(self._get(), True) + return float(dct['LINEV']) + + def get_loadpct(self): + dct = self._parse(self._get(), True) + return float(dct['LOADPCT']) - def get_onbatt(self): - return self.apc.get_onbatt() + def get_bcharge(self): + dct = self._parse(self._get(), True) + return float(dct['BCHARGE']) def get_timeleft(self): - return self.apc.get_timeleft()
      + dct = self._parse(self._get(), True) + return float(dct['TIMELEFT']) + + def get_onbatt(self): + dct = self._parse(self._get(), True) + return 'ONBATT' in dct['STATUS'] + + def get_mbattchg(self): + dct = self._parse(self._get(), True) + return float(dct['MBATTCHG']) + + def get_mintimel(self): + dct = self._parse(self._get(), True) + return float(dct['MINTIMEL']) + + def get_maxtime(self): + dct = self._parse(self._get(), True) + return float(dct['MAXTIME']) + + def get_maxlinev(self): + dct = self._parse(self._get(), True) + return float(dct['MAXLINEV']) + + def get_minlinev(self): + dct = self._parse(self._get(), True) + return float(dct['MINLINEV']) + + def get_outputv(self): + dct = self._parse(self._get(), True) + return float(dct['OUTPUTV']) + + def get_dlowbatt(self): + dct = self._parse(self._get(), True) + return float(dct['DLOWBATT']) + + def get_lotrans(self): + dct = self._parse(self._get(), True) + return float(dct['LOTRANS']) + + def get_hitrans(self): + dct = self._parse(self._get(), True) + return float(dct['HITRANS']) + + def get_itemp(self): + dct = self._parse(self._get(), True) + return float(dct['ITEMP']) + + def get_alarmdel(self): + dct = self._parse(self._get(), True) + return float(dct['ALARMDEL']) + + def get_status_dict(self): + return self._parse(self._get(), True) + + def _get(self, host="localhost", port=3551, timeout=30): + """ + Connect to the APCUPSd NIS and request its status. + """ + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + sock.connect((host, port)) + sock.send(CMD_STATUS) + buffr = "" + while not buffr.endswith(EOF): + buffr += sock.recv(BUFFER_SIZE).decode() + sock.close() + return buffr + + + def _split(self, raw_status): + """ + Split the output from get_status() into lines, removing the length and + newline chars. + """ + # Remove the EOF string, split status on the line endings (\x00), strip the + # length byte and newline chars off the beginning and end respectively. + return [x[1:-1] for x in raw_status[:-len(EOF)].split("\x00") if x] + + + def _parse(self, raw_status, strip_units=False): + """ + Split the output from get_status() into lines, clean it up and return it as + an OrderedDict. + """ + lines = self._split(raw_status) + if strip_units: + lines = self._strip_units_from_lines(lines) + # Split each line on the SEP character, strip extraneous whitespace and + # create an OrderedDict out of the keys/values. + return OrderedDict([[x.strip() for x in x.split(SEP, 1)] for x in lines]) + + + def _strip_units_from_lines(self, lines): + """ + Removes all units from the ends of the lines. + """ + for line in lines: + for unit in ALL_UNITS: + if line.endswith(" %s" % unit): + line = line[:-1-len(unit)] + yield line

      Ancestors

      +

      Methods

      +
      +
      +def get_alarmdel(self) +
      +
      +
      +
      + +Expand source code + +
      def get_alarmdel(self):
      +    dct = self._parse(self._get(), True)
      +    return float(dct['ALARMDEL'])
      +
      +
      +
      +def get_dlowbatt(self) +
      +
      +
      +
      + +Expand source code + +
      def get_dlowbatt(self):
      +    dct = self._parse(self._get(), True)
      +    return float(dct['DLOWBATT'])
      +
      +
      +
      +def get_hitrans(self) +
      +
      +
      +
      + +Expand source code + +
      def get_hitrans(self):
      +    dct = self._parse(self._get(), True)
      +    return float(dct['HITRANS'])
      +
      +
      +
      +def get_itemp(self) +
      +
      +
      +
      + +Expand source code + +
      def get_itemp(self):
      +    dct = self._parse(self._get(), True)
      +    return float(dct['ITEMP'])
      +
      +
      +
      +def get_linev(self) +
      +
      +
      +
      + +Expand source code + +
      def get_linev(self):
      +    dct = self._parse(self._get(), True)
      +    return float(dct['LINEV'])
      +
      +
      +
      +def get_loadpct(self) +
      +
      +
      +
      + +Expand source code + +
      def get_loadpct(self):
      +    dct = self._parse(self._get(), True)
      +    return float(dct['LOADPCT'])
      +
      +
      +
      +def get_lotrans(self) +
      +
      +
      +
      + +Expand source code + +
      def get_lotrans(self):
      +    dct = self._parse(self._get(), True)
      +    return float(dct['LOTRANS'])
      +
      +
      +
      +def get_maxlinev(self) +
      +
      +
      +
      + +Expand source code + +
      def get_maxlinev(self):
      +    dct = self._parse(self._get(), True)
      +    return float(dct['MAXLINEV'])
      +
      +
      +
      +def get_maxtime(self) +
      +
      +
      +
      + +Expand source code + +
      def get_maxtime(self):
      +    dct = self._parse(self._get(), True)
      +    return float(dct['MAXTIME'])
      +
      +
      +
      +def get_mbattchg(self) +
      +
      +
      +
      + +Expand source code + +
      def get_mbattchg(self):
      +    dct = self._parse(self._get(), True)
      +    return float(dct['MBATTCHG'])
      +
      +
      +
      +def get_minlinev(self) +
      +
      +
      +
      + +Expand source code + +
      def get_minlinev(self):
      +    dct = self._parse(self._get(), True)
      +    return float(dct['MINLINEV'])
      +
      +
      +
      +def get_mintimel(self) +
      +
      +
      +
      + +Expand source code + +
      def get_mintimel(self):
      +    dct = self._parse(self._get(), True)
      +    return float(dct['MINTIMEL'])
      +
      +
      +
      +def get_outputv(self) +
      +
      +
      +
      + +Expand source code + +
      def get_outputv(self):
      +    dct = self._parse(self._get(), True)
      +    return float(dct['OUTPUTV'])
      +
      +
      +
      +def get_status_dict(self) +
      +
      +
      +
      + +Expand source code + +
      def get_status_dict(self):
      +    return self._parse(self._get(), True)
      +
      +
      +

      Inherited members

      +
      +class APCError +(*args, **kwargs) +
      +
      +

      Common base class for all non-exit exceptions.

      +
      + +Expand source code + +
      class APCError(Exception):
      +    pass
      +
      +

      Ancestors

      +
        +
      • builtins.Exception
      • +
      • builtins.BaseException
      • +
      +
      class APCInterface
      @@ -484,7 +970,32 @@

      Index

    • Classes

      • +

        APCCommand

        + +
      • +
      • APCController

        + +
      • +
      • +

        APCError

      • APCInterface

        diff --git a/docs/api/egse/ups/apc/apc_cs.html b/docs/api/egse/ups/apc/apc_cs.html index 21c4ae5..e2e6306 100644 --- a/docs/api/egse/ups/apc/apc_cs.html +++ b/docs/api/egse/ups/apc/apc_cs.html @@ -281,10 +281,15 @@

        Inherited members

        • ControlServer:
        • diff --git a/docs/api/egse/ups/apc/apc_devif.html b/docs/api/egse/ups/apc/apc_devif.html deleted file mode 100644 index f064ba7..0000000 --- a/docs/api/egse/ups/apc/apc_devif.html +++ /dev/null @@ -1,468 +0,0 @@ - - - - - - -egse.ups.apc.apc_devif API documentation - - - - - - - - - - - -
          -
          -
          -

          Module egse.ups.apc.apc_devif

          -
          -
          -
          - -Expand source code - -
          from __future__ import print_function
          -
          -import logging
          -import struct
          -import time
          -import socket
          -import errno
          -import os
          -from collections import OrderedDict
          -
          -from egse.command import ClientServerCommand
          -from egse.exceptions import DeviceNotFoundError
          -from egse.settings import Settings
          -
          -logger = logging.getLogger(__name__)
          -ctrl_settings = Settings.load('APC Control Server')
          -
          -CMD_STATUS = "\x00\x06status".encode()
          -EOF = "  \n\x00\x00"
          -SEP = ":"
          -BUFFER_SIZE = 1024
          -ALL_UNITS = (
          -    "Minutes",
          -    "Seconds",
          -    "Percent",
          -    "Volts",
          -    "Watts",
          -    "Amps",
          -    "Hz",
          -    "C",
          -    "VA",
          -    "Percent Load Capacity"
          -)
          -
          -
          -class APCError(Exception):
          -    pass
          -
          -
          -class APCCommand(ClientServerCommand):
          -    def get_cmd_string(self, *args, **kwargs):
          -        out = super().get_cmd_string(*args, **kwargs)
          -        return out + '\n'
          -
          -
          -class APCDeviceInterface:
          -    def __init__(self):
          -        self._is_connected = False
          -
          -    def connect(self):
          -        self._is_connected = True
          -
          -    def disconnect(self):
          -        self._is_connected = False
          -
          -    def reconnect(self):
          -        if self._is_connected:
          -            self.disconnect()
          -        self.connect()
          -
          -    def is_connected(self):
          -        return self._is_connected
          -
          -    def get_bcharge(self):
          -        dct = self._parse(self._get(), True)
          -        return float(dct['BCHARGE'])
          -
          -    def get_onbatt(self):
          -        dct = self._parse(self._get(), True)
          -        return 'ONBATT' in dct['STATUS']
          -
          -    def get_timeleft(self):
          -        dct = self._parse(self._get(), True)
          -        return float(dct['TIMELEFT'])
          -
          -    def _get(self, host="localhost", port=3551, timeout=30):
          -        """
          -        Connect to the APCUPSd NIS and request its status.
          -        """
          -        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
          -        sock.settimeout(timeout)
          -        sock.connect((host, port))
          -        sock.send(CMD_STATUS)
          -        buffr = ""
          -        while not buffr.endswith(EOF):
          -            buffr += sock.recv(BUFFER_SIZE).decode()
          -        sock.close()
          -        return buffr
          -
          -
          -    def _split(self, raw_status):
          -        """
          -        Split the output from get_status() into lines, removing the length and
          -        newline chars.
          -        """
          -        # Remove the EOF string, split status on the line endings (\x00), strip the
          -        # length byte and newline chars off the beginning and end respectively.
          -        return [x[1:-1] for x in raw_status[:-len(EOF)].split("\x00") if x]
          -
          -
          -    def _parse(self, raw_status, strip_units=False):
          -        """
          -        Split the output from get_status() into lines, clean it up and return it as
          -        an OrderedDict.
          -        """
          -        lines = self._split(raw_status)
          -        if strip_units:
          -            lines = self._strip_units_from_lines(lines)
          -        # Split each line on the SEP character, strip extraneous whitespace and
          -        # create an OrderedDict out of the keys/values.
          -        return OrderedDict([[x.strip() for x in x.split(SEP, 1)] for x in lines])
          -
          -
          -    def _strip_units_from_lines(self, lines):
          -        """
          -        Removes all units from the ends of the lines.
          -        """
          -        for line in lines:
          -            for unit in ALL_UNITS:
          -                if line.endswith(" %s" % unit):
          -                    line = line[:-1-len(unit)]
          -            yield line
          -
          -            
          -
          -
          -
          -
          -
          -
          -
          -
          -
          -

          Classes

          -
          -
          -class APCCommand -(name, cmd, response=None, wait=None, check=None, description=None, device_method=None) -
          -
          -

          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.

          -
          - -Expand source code - -
          class APCCommand(ClientServerCommand):
          -    def get_cmd_string(self, *args, **kwargs):
          -        out = super().get_cmd_string(*args, **kwargs)
          -        return out + '\n'
          -
          -

          Ancestors

          - -

          Methods

          -
          -
          -def get_cmd_string(self, *args, **kwargs) -
          -
          -
          -
          - -Expand source code - -
          def get_cmd_string(self, *args, **kwargs):
          -    out = super().get_cmd_string(*args, **kwargs)
          -    return out + '\n'
          -
          -
          -
          -

          Inherited members

          - -
          -
          -class APCDeviceInterface -
          -
          -
          -
          - -Expand source code - -
          class APCDeviceInterface:
          -    def __init__(self):
          -        self._is_connected = False
          -
          -    def connect(self):
          -        self._is_connected = True
          -
          -    def disconnect(self):
          -        self._is_connected = False
          -
          -    def reconnect(self):
          -        if self._is_connected:
          -            self.disconnect()
          -        self.connect()
          -
          -    def is_connected(self):
          -        return self._is_connected
          -
          -    def get_bcharge(self):
          -        dct = self._parse(self._get(), True)
          -        return float(dct['BCHARGE'])
          -
          -    def get_onbatt(self):
          -        dct = self._parse(self._get(), True)
          -        return 'ONBATT' in dct['STATUS']
          -
          -    def get_timeleft(self):
          -        dct = self._parse(self._get(), True)
          -        return float(dct['TIMELEFT'])
          -
          -    def _get(self, host="localhost", port=3551, timeout=30):
          -        """
          -        Connect to the APCUPSd NIS and request its status.
          -        """
          -        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
          -        sock.settimeout(timeout)
          -        sock.connect((host, port))
          -        sock.send(CMD_STATUS)
          -        buffr = ""
          -        while not buffr.endswith(EOF):
          -            buffr += sock.recv(BUFFER_SIZE).decode()
          -        sock.close()
          -        return buffr
          -
          -
          -    def _split(self, raw_status):
          -        """
          -        Split the output from get_status() into lines, removing the length and
          -        newline chars.
          -        """
          -        # Remove the EOF string, split status on the line endings (\x00), strip the
          -        # length byte and newline chars off the beginning and end respectively.
          -        return [x[1:-1] for x in raw_status[:-len(EOF)].split("\x00") if x]
          -
          -
          -    def _parse(self, raw_status, strip_units=False):
          -        """
          -        Split the output from get_status() into lines, clean it up and return it as
          -        an OrderedDict.
          -        """
          -        lines = self._split(raw_status)
          -        if strip_units:
          -            lines = self._strip_units_from_lines(lines)
          -        # Split each line on the SEP character, strip extraneous whitespace and
          -        # create an OrderedDict out of the keys/values.
          -        return OrderedDict([[x.strip() for x in x.split(SEP, 1)] for x in lines])
          -
          -
          -    def _strip_units_from_lines(self, lines):
          -        """
          -        Removes all units from the ends of the lines.
          -        """
          -        for line in lines:
          -            for unit in ALL_UNITS:
          -                if line.endswith(" %s" % unit):
          -                    line = line[:-1-len(unit)]
          -            yield line
          -
          -

          Methods

          -
          -
          -def connect(self) -
          -
          -
          -
          - -Expand source code - -
          def connect(self):
          -    self._is_connected = True
          -
          -
          -
          -def disconnect(self) -
          -
          -
          -
          - -Expand source code - -
          def disconnect(self):
          -    self._is_connected = False
          -
          -
          -
          -def get_bcharge(self) -
          -
          -
          -
          - -Expand source code - -
          def get_bcharge(self):
          -    dct = self._parse(self._get(), True)
          -    return float(dct['BCHARGE'])
          -
          -
          -
          -def get_onbatt(self) -
          -
          -
          -
          - -Expand source code - -
          def get_onbatt(self):
          -    dct = self._parse(self._get(), True)
          -    return 'ONBATT' in dct['STATUS']
          -
          -
          -
          -def get_timeleft(self) -
          -
          -
          -
          - -Expand source code - -
          def get_timeleft(self):
          -    dct = self._parse(self._get(), True)
          -    return float(dct['TIMELEFT'])
          -
          -
          -
          -def is_connected(self) -
          -
          -
          -
          - -Expand source code - -
          def is_connected(self):
          -    return self._is_connected
          -
          -
          -
          -def reconnect(self) -
          -
          -
          -
          - -Expand source code - -
          def reconnect(self):
          -    if self._is_connected:
          -        self.disconnect()
          -    self.connect()
          -
          -
          -
          -
          -
          -class APCError -(*args, **kwargs) -
          -
          -

          Common base class for all non-exit exceptions.

          -
          - -Expand source code - -
          class APCError(Exception):
          -    pass
          -
          -

          Ancestors

          -
            -
          • builtins.Exception
          • -
          • builtins.BaseException
          • -
          -
          -
          -
          -
          - -
          - - - \ No newline at end of file diff --git a/docs/api/egse/ups/apc/apc_protocol.html b/docs/api/egse/ups/apc/apc_protocol.html index bcd8135..f44f0d5 100644 --- a/docs/api/egse/ups/apc/apc_protocol.html +++ b/docs/api/egse/ups/apc/apc_protocol.html @@ -38,7 +38,7 @@

          Module egse.ups.apc.apc_protocol

          from egse.ups.apc.apc import APCController from egse.ups.apc.apc import APCInterface from egse.ups.apc.apc import APCSimulator -from egse.ups.apc.apc_devif import APCCommand +from egse.ups.apc.apc import APCCommand from egse.system import format_datetime from egse.zmq_ser import bind_address @@ -46,12 +46,6 @@

          Module egse.ups.apc.apc_protocol

          COMMAND_SETTINGS = Settings.load(filename='apc.yaml') -FDIR_PREFIX = 800 - -gauge_battchrage = Gauge('GSRON_UPS_CHARGE', '') -gauge_onbatt = Gauge('GSRON_UPS_ON_BATT', '') -gauge_timeleft = Gauge('GSRON_UPS_TIME_LEFT', '') - class APCProtocol(CommandProtocol): def __init__(self, control_server:ControlServer): super().__init__() @@ -65,6 +59,31 @@

          Module egse.ups.apc.apc_protocol

          self.load_commands(COMMAND_SETTINGS.Commands, APCCommand, APCInterface) self.build_device_method_lookup_table(self.apc) + + self.gauge_linev = Gauge('GSRON_UPS_LINEV', '') + self.gauge_loadpct = Gauge('GSRON_UPS_LOADPCT', '') + self.gauge_bcharge = Gauge('GSRON_UPS_BCHARGE', '') + self.gauge_timeleft = Gauge('GSRON_UPS_TIMELEFT', '') + self.gauge_mbattchg = Gauge('GSRON_UPS_MBATTCHG', '') + self.gauge_mintimel = Gauge('GSRON_UPS_MINTIMEL', '') + self.gauge_maxtime = Gauge('GSRON_UPS_MAXTIME', '') + self.gauge_maxlinev = Gauge('GSRON_UPS_MAXLINEV', '') + self.gauge_minlinev = Gauge('GSRON_UPS_MINLINEV', '') + self.gauge_outputv = Gauge('GSRON_UPS_OUTPUTV', '') + self.gauge_dlowbatt = Gauge('GSRON_UPS_DLOWBATT', '') + self.gauge_lotrans = Gauge('GSRON_UPS_LOTRANS', '') + self.gauge_hitrans = Gauge('GSRON_UPS_HITRANS', '') + self.gauge_itemp = Gauge('GSRON_UPS_ITEMP', '') + self.gauge_alarmdel = Gauge('GSRON_UPS_ALARMDEL', '') + self.gauge_battv = Gauge('GSRON_UPS_BATTV', '') + self.gauge_linefreq = Gauge('GSRON_UPS_LINEFREQ', '') + self.gauge_lastxfer = Gauge('GSRON_UPS_LASTXFER', '') + self.gauge_numxfers = Gauge('GSRON_UPS_NUMXFERS', '') + self.gauge_tonbatt = Gauge('GSRON_UPS_TONBATT', '') + self.gauge_cumonbatt = Gauge('GSRON_UPS_CUMONBATT', '') + self.gauge_xoffbatt = Gauge('GSRON_UPS_XOFFBATT', '') + self.gauge_statflag = Gauge('GSRON_UPS_STATFLAG', '') + self.gauge_onbatt = Gauge('GSRON_UPS_ONBATT', '') def get_bind_address(self): @@ -79,23 +98,57 @@

          Module egse.ups.apc.apc_protocol

          hk_dict = {'timestamp': format_datetime()} try: - hk_dict[f"GSRON_UPS_CHARGE"] = self.apc.get_bcharge() - hk_dict[f"GSRON_UPS_ON_BATT"] = self.apc.get_onbatt() - hk_dict[f"GSRON_UPS_TIME_LEFT"] = self.apc.get_timeleft() + dct = self.apc.get_status_dict() + hk_dict[f'GSRON_UPS_LINEV'] = dct['LINEV'] + hk_dict[f'GSRON_UPS_LOADPCT'] = dct['LOADPCT'] + hk_dict[f'GSRON_UPS_BCHARGE'] = dct['BCHARGE'] + hk_dict[f'GSRON_UPS_TIMELEFT'] = dct['TIMELEFT'] + hk_dict[f'GSRON_UPS_MBATTCHG'] = dct['MBATTCHG'] + hk_dict[f'GSRON_UPS_MINTIMEL'] = dct['MINTIMEL'] + hk_dict[f'GSRON_UPS_MAXTIME'] = dct['MAXTIME'] + hk_dict[f'GSRON_UPS_MAXLINEV'] = dct['MAXLINEV'] + hk_dict[f'GSRON_UPS_MINLINEV'] = dct['MINLINEV'] + hk_dict[f'GSRON_UPS_OUTPUTV'] = dct['OUTPUTV'] + hk_dict[f'GSRON_UPS_DLOWBATT'] = dct['DLOWBATT'] + hk_dict[f'GSRON_UPS_LOTRANS'] = dct['LOTRANS'] + hk_dict[f'GSRON_UPS_HITRANS'] = dct['HITRANS'] + hk_dict[f'GSRON_UPS_ITEMP'] = dct['ITEMP'] + hk_dict[f'GSRON_UPS_ALARMDEL'] = dct['ALARMDEL'] + hk_dict[f'GSRON_UPS_BATTV'] = dct['BATTV'] + hk_dict[f'GSRON_UPS_LINEFREQ'] = dct['LINEFREQ'] + hk_dict[f'GSRON_UPS_NUMXFERS'] = dct['NUMXFERS'] + hk_dict[f'GSRON_UPS_TONBATT'] = dct['TONBATT'] + hk_dict[f'GSRON_UPS_CUMONBATT'] = dct['CUMONBATT'] + hk_dict[f'GSRON_UPS_STATFLAG'] = int(dct['STATFLAG'], 16) + hk_dict[f'GSRON_UPS_ONBATT'] = 'ONBATT' in dct['STATUS'] except Exception as exc: - logger.error(f'failed to get HK ({exc})') - - # with FdirManagerProxy() as fdir: - # fdir_code = FDIR_PREFIX + 1 - # fdir.signal_fdir(fdir_code) - # logger.warning(f'asserted fdir signal: {fdir_code}') + logger.exception("failed to get HK: %s", exc) return hk_dict - - gauge_battchrage.set(hk_dict[f"GSRON_UPS_CHARGE"]) - gauge_onbatt.set(hk_dict[f"GSRON_UPS_ON_BATT"]) - gauge_timeleft.set(hk_dict[f"GSRON_UPS_TIME_LEFT"]) + + self.gauge_linev.set(hk_dict[f'GSRON_UPS_LINEV']) + self.gauge_loadpct.set(hk_dict[f'GSRON_UPS_LOADPCT']) + self.gauge_bcharge.set(hk_dict[f'GSRON_UPS_BCHARGE']) + self.gauge_timeleft.set(hk_dict[f'GSRON_UPS_TIMELEFT']) + self.gauge_mbattchg.set(hk_dict[f'GSRON_UPS_MBATTCHG']) + self.gauge_mintimel.set(hk_dict[f'GSRON_UPS_MINTIMEL']) + self.gauge_maxtime.set(hk_dict[f'GSRON_UPS_MAXTIME']) + self.gauge_maxlinev.set(hk_dict[f'GSRON_UPS_MAXLINEV']) + self.gauge_minlinev.set(hk_dict[f'GSRON_UPS_MINLINEV']) + self.gauge_outputv.set(hk_dict[f'GSRON_UPS_OUTPUTV']) + self.gauge_dlowbatt.set(hk_dict[f'GSRON_UPS_DLOWBATT']) + self.gauge_lotrans.set(hk_dict[f'GSRON_UPS_LOTRANS']) + self.gauge_hitrans.set(hk_dict[f'GSRON_UPS_HITRANS']) + self.gauge_itemp.set(hk_dict[f'GSRON_UPS_ITEMP']) + self.gauge_alarmdel.set(hk_dict[f'GSRON_UPS_ALARMDEL']) + self.gauge_battv.set(hk_dict[f'GSRON_UPS_BATTV']) + self.gauge_linefreq.set(hk_dict[f'GSRON_UPS_LINEFREQ']) + self.gauge_numxfers.set(hk_dict[f'GSRON_UPS_NUMXFERS']) + self.gauge_tonbatt.set(hk_dict[f'GSRON_UPS_TONBATT']) + self.gauge_cumonbatt.set(hk_dict[f'GSRON_UPS_CUMONBATT']) + self.gauge_statflag.set(hk_dict[f'GSRON_UPS_STATFLAG']) + self.gauge_onbatt.set(hk_dict[f'GSRON_UPS_ONBATT']) return hk_dict @@ -139,6 +192,31 @@

          Classes

          self.load_commands(COMMAND_SETTINGS.Commands, APCCommand, APCInterface) self.build_device_method_lookup_table(self.apc) + + self.gauge_linev = Gauge('GSRON_UPS_LINEV', '') + self.gauge_loadpct = Gauge('GSRON_UPS_LOADPCT', '') + self.gauge_bcharge = Gauge('GSRON_UPS_BCHARGE', '') + self.gauge_timeleft = Gauge('GSRON_UPS_TIMELEFT', '') + self.gauge_mbattchg = Gauge('GSRON_UPS_MBATTCHG', '') + self.gauge_mintimel = Gauge('GSRON_UPS_MINTIMEL', '') + self.gauge_maxtime = Gauge('GSRON_UPS_MAXTIME', '') + self.gauge_maxlinev = Gauge('GSRON_UPS_MAXLINEV', '') + self.gauge_minlinev = Gauge('GSRON_UPS_MINLINEV', '') + self.gauge_outputv = Gauge('GSRON_UPS_OUTPUTV', '') + self.gauge_dlowbatt = Gauge('GSRON_UPS_DLOWBATT', '') + self.gauge_lotrans = Gauge('GSRON_UPS_LOTRANS', '') + self.gauge_hitrans = Gauge('GSRON_UPS_HITRANS', '') + self.gauge_itemp = Gauge('GSRON_UPS_ITEMP', '') + self.gauge_alarmdel = Gauge('GSRON_UPS_ALARMDEL', '') + self.gauge_battv = Gauge('GSRON_UPS_BATTV', '') + self.gauge_linefreq = Gauge('GSRON_UPS_LINEFREQ', '') + self.gauge_lastxfer = Gauge('GSRON_UPS_LASTXFER', '') + self.gauge_numxfers = Gauge('GSRON_UPS_NUMXFERS', '') + self.gauge_tonbatt = Gauge('GSRON_UPS_TONBATT', '') + self.gauge_cumonbatt = Gauge('GSRON_UPS_CUMONBATT', '') + self.gauge_xoffbatt = Gauge('GSRON_UPS_XOFFBATT', '') + self.gauge_statflag = Gauge('GSRON_UPS_STATFLAG', '') + self.gauge_onbatt = Gauge('GSRON_UPS_ONBATT', '') def get_bind_address(self): @@ -153,23 +231,57 @@

          Classes

          hk_dict = {'timestamp': format_datetime()} try: - hk_dict[f"GSRON_UPS_CHARGE"] = self.apc.get_bcharge() - hk_dict[f"GSRON_UPS_ON_BATT"] = self.apc.get_onbatt() - hk_dict[f"GSRON_UPS_TIME_LEFT"] = self.apc.get_timeleft() + dct = self.apc.get_status_dict() + hk_dict[f'GSRON_UPS_LINEV'] = dct['LINEV'] + hk_dict[f'GSRON_UPS_LOADPCT'] = dct['LOADPCT'] + hk_dict[f'GSRON_UPS_BCHARGE'] = dct['BCHARGE'] + hk_dict[f'GSRON_UPS_TIMELEFT'] = dct['TIMELEFT'] + hk_dict[f'GSRON_UPS_MBATTCHG'] = dct['MBATTCHG'] + hk_dict[f'GSRON_UPS_MINTIMEL'] = dct['MINTIMEL'] + hk_dict[f'GSRON_UPS_MAXTIME'] = dct['MAXTIME'] + hk_dict[f'GSRON_UPS_MAXLINEV'] = dct['MAXLINEV'] + hk_dict[f'GSRON_UPS_MINLINEV'] = dct['MINLINEV'] + hk_dict[f'GSRON_UPS_OUTPUTV'] = dct['OUTPUTV'] + hk_dict[f'GSRON_UPS_DLOWBATT'] = dct['DLOWBATT'] + hk_dict[f'GSRON_UPS_LOTRANS'] = dct['LOTRANS'] + hk_dict[f'GSRON_UPS_HITRANS'] = dct['HITRANS'] + hk_dict[f'GSRON_UPS_ITEMP'] = dct['ITEMP'] + hk_dict[f'GSRON_UPS_ALARMDEL'] = dct['ALARMDEL'] + hk_dict[f'GSRON_UPS_BATTV'] = dct['BATTV'] + hk_dict[f'GSRON_UPS_LINEFREQ'] = dct['LINEFREQ'] + hk_dict[f'GSRON_UPS_NUMXFERS'] = dct['NUMXFERS'] + hk_dict[f'GSRON_UPS_TONBATT'] = dct['TONBATT'] + hk_dict[f'GSRON_UPS_CUMONBATT'] = dct['CUMONBATT'] + hk_dict[f'GSRON_UPS_STATFLAG'] = int(dct['STATFLAG'], 16) + hk_dict[f'GSRON_UPS_ONBATT'] = 'ONBATT' in dct['STATUS'] except Exception as exc: - logger.error(f'failed to get HK ({exc})') - - # with FdirManagerProxy() as fdir: - # fdir_code = FDIR_PREFIX + 1 - # fdir.signal_fdir(fdir_code) - # logger.warning(f'asserted fdir signal: {fdir_code}') + logger.exception("failed to get HK: %s", exc) return hk_dict - - gauge_battchrage.set(hk_dict[f"GSRON_UPS_CHARGE"]) - gauge_onbatt.set(hk_dict[f"GSRON_UPS_ON_BATT"]) - gauge_timeleft.set(hk_dict[f"GSRON_UPS_TIME_LEFT"]) + + self.gauge_linev.set(hk_dict[f'GSRON_UPS_LINEV']) + self.gauge_loadpct.set(hk_dict[f'GSRON_UPS_LOADPCT']) + self.gauge_bcharge.set(hk_dict[f'GSRON_UPS_BCHARGE']) + self.gauge_timeleft.set(hk_dict[f'GSRON_UPS_TIMELEFT']) + self.gauge_mbattchg.set(hk_dict[f'GSRON_UPS_MBATTCHG']) + self.gauge_mintimel.set(hk_dict[f'GSRON_UPS_MINTIMEL']) + self.gauge_maxtime.set(hk_dict[f'GSRON_UPS_MAXTIME']) + self.gauge_maxlinev.set(hk_dict[f'GSRON_UPS_MAXLINEV']) + self.gauge_minlinev.set(hk_dict[f'GSRON_UPS_MINLINEV']) + self.gauge_outputv.set(hk_dict[f'GSRON_UPS_OUTPUTV']) + self.gauge_dlowbatt.set(hk_dict[f'GSRON_UPS_DLOWBATT']) + self.gauge_lotrans.set(hk_dict[f'GSRON_UPS_LOTRANS']) + self.gauge_hitrans.set(hk_dict[f'GSRON_UPS_HITRANS']) + self.gauge_itemp.set(hk_dict[f'GSRON_UPS_ITEMP']) + self.gauge_alarmdel.set(hk_dict[f'GSRON_UPS_ALARMDEL']) + self.gauge_battv.set(hk_dict[f'GSRON_UPS_BATTV']) + self.gauge_linefreq.set(hk_dict[f'GSRON_UPS_LINEFREQ']) + self.gauge_numxfers.set(hk_dict[f'GSRON_UPS_NUMXFERS']) + self.gauge_tonbatt.set(hk_dict[f'GSRON_UPS_TONBATT']) + self.gauge_cumonbatt.set(hk_dict[f'GSRON_UPS_CUMONBATT']) + self.gauge_statflag.set(hk_dict[f'GSRON_UPS_STATFLAG']) + self.gauge_onbatt.set(hk_dict[f'GSRON_UPS_ONBATT']) return hk_dict diff --git a/docs/api/egse/ups/apc/index.html b/docs/api/egse/ups/apc/index.html index a816b1f..6716242 100644 --- a/docs/api/egse/ups/apc/index.html +++ b/docs/api/egse/ups/apc/index.html @@ -34,10 +34,6 @@

          Sub-modules

          -
          egse.ups.apc.apc_devif
          -
          -
          -
          egse.ups.apc.apc_protocol
          @@ -66,7 +62,6 @@

          Index

          diff --git a/docs/api/egse/vacuum/beaglebone/beaglebone_cs.html b/docs/api/egse/vacuum/beaglebone/beaglebone_cs.html index 26ee5ad..45dd5d1 100644 --- a/docs/api/egse/vacuum/beaglebone/beaglebone_cs.html +++ b/docs/api/egse/vacuum/beaglebone/beaglebone_cs.html @@ -308,10 +308,15 @@

          Inherited members

          • ControlServer:
          • diff --git a/docs/api/egse/vacuum/beaglebone/beaglebone_devif.html b/docs/api/egse/vacuum/beaglebone/beaglebone_devif.html index 268a1ad..8702252 100644 --- a/docs/api/egse/vacuum/beaglebone/beaglebone_devif.html +++ b/docs/api/egse/vacuum/beaglebone/beaglebone_devif.html @@ -27,16 +27,11 @@

            Module egse.vacuum.beaglebone.beaglebone_devifExpand source code
            import logging
            -import struct
            -import time
            -import socket
            -import errno
             import os
             
             from egse.command import ClientServerCommand
             from egse.settings import Settings
             
            -
             logger = logging.getLogger(__name__)
             ctrl_settings = Settings.load('BeagleBone Valve Control Server')
             
            diff --git a/docs/api/egse/vacuum/beaglebone/beaglebone_protocol.html b/docs/api/egse/vacuum/beaglebone/beaglebone_protocol.html
            index a62c460..b04373b 100644
            --- a/docs/api/egse/vacuum/beaglebone/beaglebone_protocol.html
            +++ b/docs/api/egse/vacuum/beaglebone/beaglebone_protocol.html
            @@ -31,7 +31,6 @@ 

            Module egse.vacuum.beaglebone.beaglebone_protocol from egse.control import ControlServer from egse.protocol import CommandProtocol from egse.settings import Settings -# from egse.fdir.fdir_manager import FdirManagerProxy from prometheus_client import Gauge diff --git a/docs/api/egse/vacuum/beaglebone/beaglebone_ui.html b/docs/api/egse/vacuum/beaglebone/beaglebone_ui.html index feed96f..4b17cc8 100644 --- a/docs/api/egse/vacuum/beaglebone/beaglebone_ui.html +++ b/docs/api/egse/vacuum/beaglebone/beaglebone_ui.html @@ -224,8 +224,10 @@

            Module egse.vacuum.beaglebone.beaglebone_ui

            self.ventClose = QPushButton("Close") self.ventClose.setObjectName("MV002-close") + self.ventClose.setEnabled(False) self.ventOpen = QPushButton("Open") self.ventOpen.setObjectName("MV002-open") + self.ventOpen.setEnabled(False) self.gateClose = QPushButton("Close") self.gateClose.setObjectName("MV001-close") self.gateOpen = QPushButton("Open") @@ -378,9 +380,9 @@

            Module egse.vacuum.beaglebone.beaglebone_ui

            elif sender.text() == 'Unlock': msg = QMessageBox() msg.setIcon(QMessageBox.Warning) - msg.setWindowTitle("Vent GUI lock") - msg.setText("You are about to unlock the vent controller GUI") - msg.setInformativeText("Are you sure that you want to unlock the vent controller GUI?") + msg.setWindowTitle("Valve GUI lock") + msg.setText("You are about to unlock the valve controller GUI") + msg.setInformativeText("Are you sure that you want to unlock the valve controller GUI?") msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Abort) retval = msg.exec_() @@ -395,7 +397,6 @@

            Module egse.vacuum.beaglebone.beaglebone_ui

            def on_open_click(self): sender = self.sender() code = sender.objectName().split('-')[0] - # print(sender.objectName()) if code == 'MV002': msg = QMessageBox() msg.setIcon(QMessageBox.Warning) @@ -421,6 +422,8 @@

            Module egse.vacuum.beaglebone.beaglebone_ui

            monitoring_info.pop('GSRON_VALVE_INTERLOCK_DOOR') if self.guiEnabled: for key, value in monitoring_info.items(): + if 'MV002' in key: + continue code = key.split("_")[-1] if self.remote[code]: for child in self.findChildren(QPushButton): @@ -462,6 +465,8 @@

            Module egse.vacuum.beaglebone.beaglebone_ui

            def on_remote_status_change(self, monitoring_info): if self.guiEnabled: for key, value in monitoring_info.items(): + if 'MV002' in key: + continue code = key.split("_")[-1] if self.remote[code] != value: if value: @@ -741,8 +746,8 @@

            Classes

            class ValveControl
            -

            QGroupBox(parent: QWidget = None) -QGroupBox(str, parent: QWidget = None)

            +

            QGroupBox(parent: typing.Optional[QWidget] = None) +QGroupBox(title: str, parent: typing.Optional[QWidget] = None)

            Expand source code @@ -836,8 +841,10 @@

            Classes

            self.ventClose = QPushButton("Close") self.ventClose.setObjectName("MV002-close") + self.ventClose.setEnabled(False) self.ventOpen = QPushButton("Open") self.ventOpen.setObjectName("MV002-open") + self.ventOpen.setEnabled(False) self.gateClose = QPushButton("Close") self.gateClose.setObjectName("MV001-close") self.gateOpen = QPushButton("Open") @@ -930,7 +937,7 @@

            Ancestors

            class ValveMonitoringWorker
            -

            QObject(parent: QObject = None)

            +

            QObject(parent: typing.Optional[QObject] = None)

            Expand source code @@ -1498,7 +1505,7 @@

            Methods

            class ValveUIView
            -

            QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

            +

            QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

            Expand source code @@ -1577,9 +1584,9 @@

            Methods

            elif sender.text() == 'Unlock': msg = QMessageBox() msg.setIcon(QMessageBox.Warning) - msg.setWindowTitle("Vent GUI lock") - msg.setText("You are about to unlock the vent controller GUI") - msg.setInformativeText("Are you sure that you want to unlock the vent controller GUI?") + msg.setWindowTitle("Valve GUI lock") + msg.setText("You are about to unlock the valve controller GUI") + msg.setInformativeText("Are you sure that you want to unlock the valve controller GUI?") msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Abort) retval = msg.exec_() @@ -1594,7 +1601,6 @@

            Methods

            def on_open_click(self): sender = self.sender() code = sender.objectName().split('-')[0] - # print(sender.objectName()) if code == 'MV002': msg = QMessageBox() msg.setIcon(QMessageBox.Warning) @@ -1620,6 +1626,8 @@

            Methods

            monitoring_info.pop('GSRON_VALVE_INTERLOCK_DOOR') if self.guiEnabled: for key, value in monitoring_info.items(): + if 'MV002' in key: + continue code = key.split("_")[-1] if self.remote[code]: for child in self.findChildren(QPushButton): @@ -1661,6 +1669,8 @@

            Methods

            def on_remote_status_change(self, monitoring_info): if self.guiEnabled: for key, value in monitoring_info.items(): + if 'MV002' in key: + continue code = key.split("_")[-1] if self.remote[code] != value: if value: @@ -1707,7 +1717,7 @@

            Methods

            def closeEvent(self, close_event: PyQt5.QtGui.QCloseEvent) ‑> None
            -

            closeEvent(self, QCloseEvent)

            +

            closeEvent(self, a0: typing.Optional[QCloseEvent])

            Expand source code @@ -1766,6 +1776,8 @@

            Methods

            monitoring_info.pop('GSRON_VALVE_INTERLOCK_DOOR') if self.guiEnabled: for key, value in monitoring_info.items(): + if 'MV002' in key: + continue code = key.split("_")[-1] if self.remote[code]: for child in self.findChildren(QPushButton): @@ -1824,9 +1836,9 @@

            Methods

            elif sender.text() == 'Unlock': msg = QMessageBox() msg.setIcon(QMessageBox.Warning) - msg.setWindowTitle("Vent GUI lock") - msg.setText("You are about to unlock the vent controller GUI") - msg.setInformativeText("Are you sure that you want to unlock the vent controller GUI?") + msg.setWindowTitle("Valve GUI lock") + msg.setText("You are about to unlock the valve controller GUI") + msg.setInformativeText("Are you sure that you want to unlock the valve controller GUI?") msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Abort) retval = msg.exec_() @@ -1849,7 +1861,6 @@

            Methods

            def on_open_click(self):
                 sender = self.sender()
                 code = sender.objectName().split('-')[0]
            -    # print(sender.objectName())
                 if code == 'MV002':
                     msg = QMessageBox()
                     msg.setIcon(QMessageBox.Warning)
            @@ -1879,6 +1890,8 @@ 

            Methods

            def on_remote_status_change(self, monitoring_info):
                 if self.guiEnabled:
                     for key, value in monitoring_info.items():
            +            if 'MV002' in key:
            +                continue
                         code  = key.split("_")[-1]
                         if self.remote[code] != value:
                             if value:
            diff --git a/docs/api/egse/vacuum/instrutech/igm402.html b/docs/api/egse/vacuum/instrutech/igm402.html
            index 6a05c2b..7bbc013 100644
            --- a/docs/api/egse/vacuum/instrutech/igm402.html
            +++ b/docs/api/egse/vacuum/instrutech/igm402.html
            @@ -30,20 +30,19 @@ 

            Module egse.vacuum.instrutech.igm402

            from prometheus_client import Gauge -from egse.control import ControlServer from egse.command import ClientServerCommand +from egse.control import ControlServer from egse.protocol import CommandProtocol from egse.proxy import Proxy from egse.settings import Settings -from egse.setup import get_setup -from egse.zmq_ser import bind_address +from egse.setup import load_setup from egse.system import format_datetime -from egse.vacuum.instrutech.igm402_interface import Igm402Interface from egse.vacuum.instrutech.igm402_controller import Igm402Controller +from egse.vacuum.instrutech.igm402_interface import Igm402Interface from egse.vacuum.instrutech.igm402_simulator import Igm402Simulator +from egse.zmq_ser import bind_address from egse.zmq_ser import connect_address - LOGGER = logging.getLogger(__name__) CTRL_SETTINGS = Settings.load("InstruTech IGM402 Control Server") @@ -75,7 +74,7 @@

            Module egse.vacuum.instrutech.igm402

            self.build_device_method_lookup_table(self.dev) # Get calibration from setup - self.setup = get_setup() + self.setup = load_setup() # move to parent class? @@ -195,7 +194,7 @@

            Inherited members

            self.build_device_method_lookup_table(self.dev) # Get calibration from setup - self.setup = get_setup() + self.setup = load_setup() # move to parent class? diff --git a/docs/api/egse/vacuum/instrutech/igm402_controller.html b/docs/api/egse/vacuum/instrutech/igm402_controller.html index a95a6d0..39524df 100644 --- a/docs/api/egse/vacuum/instrutech/igm402_controller.html +++ b/docs/api/egse/vacuum/instrutech/igm402_controller.html @@ -27,16 +27,12 @@

            Module egse.vacuum.instrutech.igm402_controllerExpand source code

            import logging
            -import time
            -from functools import partial
            -import random
             
            -import yaml
            +import time
             
             from egse.serialdevice import SerialDevice
            -from egse.command import Command
             from egse.settings import Settings
            -from egse.setup import get_setup
            +from egse.setup import load_setup
             from egse.vacuum.instrutech.igm402_interface import Igm402Interface
             
             logger = logging.getLogger(__name__)
            @@ -58,7 +54,7 @@ 

            Module egse.vacuum.instrutech.igm402_controllerClasses

            self._address = DEVICE_SETTINGS.ADDRESS if address is None else address # Get calibration from setup - setup = get_setup() + setup = load_setup() # Initialize the parent class with the port and baudrate super().__init__(port=self._port, baudrate=self._baudrate) diff --git a/docs/api/egse/vacuum/instrutech/igm402_cs.html b/docs/api/egse/vacuum/instrutech/igm402_cs.html index 0c513e6..bf0d29f 100644 --- a/docs/api/egse/vacuum/instrutech/igm402_cs.html +++ b/docs/api/egse/vacuum/instrutech/igm402_cs.html @@ -295,10 +295,15 @@

            Inherited members

            • ControlServer:
            • diff --git a/docs/api/egse/vacuum/keller/leo3.html b/docs/api/egse/vacuum/keller/leo3.html index 0583e43..095b836 100644 --- a/docs/api/egse/vacuum/keller/leo3.html +++ b/docs/api/egse/vacuum/keller/leo3.html @@ -30,20 +30,18 @@

              Module egse.vacuum.keller.leo3

              from prometheus_client import Gauge -from egse.control import ControlServer from egse.command import ClientServerCommand +from egse.control import ControlServer from egse.protocol import CommandProtocol from egse.proxy import Proxy from egse.settings import Settings -from egse.setup import get_setup -from egse.zmq_ser import bind_address from egse.system import format_datetime -from egse.vacuum.keller.leo3_interface import Leo3Interface from egse.vacuum.keller.leo3_controller import Leo3Controller from egse.vacuum.keller.leo3_controller import Leo3Simulator +from egse.vacuum.keller.leo3_interface import Leo3Interface +from egse.zmq_ser import bind_address from egse.zmq_ser import connect_address - LOGGER = logging.getLogger(__name__) CTRL_SETTINGS = Settings.load("KELLER Leo3 Control Server") diff --git a/docs/api/egse/vacuum/keller/leo3_controller.html b/docs/api/egse/vacuum/keller/leo3_controller.html index 41b7c0d..4715f38 100644 --- a/docs/api/egse/vacuum/keller/leo3_controller.html +++ b/docs/api/egse/vacuum/keller/leo3_controller.html @@ -28,10 +28,8 @@

              Module egse.vacuum.keller.leo3_controller

            import logging
             
            -from egse.vacuum.keller.kellerBus import kellerBus
            -from egse.command import Command
             from egse.settings import Settings
            -from egse.setup import get_setup
            +from egse.vacuum.keller.kellerBus import kellerBus
             from egse.vacuum.keller.leo3_interface import Leo3Interface
             
             logger = logging.getLogger(__name__)
            diff --git a/docs/api/egse/vacuum/keller/leo3_cs.html b/docs/api/egse/vacuum/keller/leo3_cs.html
            index 27f69aa..ed1d6b9 100644
            --- a/docs/api/egse/vacuum/keller/leo3_cs.html
            +++ b/docs/api/egse/vacuum/keller/leo3_cs.html
            @@ -283,10 +283,15 @@ 

            Inherited members

            import logging
             
            +from prometheus_client import Gauge
            +
             from egse.control import ControlServer
             from egse.command import ClientServerCommand
             from egse.protocol import CommandProtocol
            @@ -35,23 +37,23 @@ 

            Module egse.vacuum.mks.vacscan

            from egse.settings import Settings from egse.zmq_ser import bind_address from egse.system import format_datetime -from egse.vacuum.mks.vacscan_interface import VacscanInterface -from egse.vacuum.mks.vacscan_controller import VacscanController -from egse.vacuum.mks.vacscan_simulator import VacscanSimulator from egse.zmq_ser import connect_address +from egse.vacuum.mks.evision_interface import EvisionInterface +from egse.vacuum.mks.evision_devif import EvisionDriver +from egse.vacuum.mks.evision_simulator import EvisionSimulator -LOGGER = logging.getLogger(__name__) +logger = logging.getLogger(__name__) -DEVICE_SETTINGS = Settings.load(filename="vacscan.yaml") -CTRL_SETTINGS = Settings.load("MKS Vacscan Control Server") +DEVICE_SETTINGS = Settings.load(filename="evision.yaml") +CTRL_SETTINGS = Settings.load("MKS E-Vision RGA Control Server") -class VacscanCommand(ClientServerCommand): +class EvisionCommand(ClientServerCommand): pass -class VacscanProtocol(CommandProtocol): +class EvisionProtocol(CommandProtocol): def __init__(self, control_server: ControlServer): @@ -59,13 +61,18 @@

            Module egse.vacuum.mks.vacscan

            self.control_server = control_server if Settings.simulation_mode(): - self.dev = VacscanSimulator() + self.dev = EvisionSimulator() else: - self.dev = VacscanController() + self.dev = EvisionDriver() - self.load_commands(DEVICE_SETTINGS.Commands, VacscanCommand, VacscanInterface) + self.load_commands(DEVICE_SETTINGS.Commands, EvisionCommand, EvisionInterface) self.build_device_method_lookup_table(self.dev) - + + self.dev.control_sensor() + self.dev.filament_status() + self.dev.rga_status() + + self.massreading_gauges = [Gauge(f'GSRON_EVISION_MASSREADING_{i}', '') for i in range(0, 200)] # move to parent class? def get_bind_address(self): @@ -77,18 +84,29 @@

            Module egse.vacuum.mks.vacscan

            def get_status(self): status_dict = super().get_status() - + + status_dict['ScanStatus'] = self.dev.get_scan_status() + status_dict['RGAStatus'] = self.dev.get_rga_status() + status_dict['FilamentStatus'] = self.dev.get_filament_status() + status_dict['MassReading'] = self.dev.get_mass_reading() return status_dict def get_housekeeping(self) -> dict: result = dict() result["timestamp"] = format_datetime() - + + for i in range(0, 200): + column_name = f'GSRON_EVISION_MASSREADING_{i}' + + result[column_name] = self.dev.get_mass_reading()[i] + + self.massreading_gauges[i].set(self.dev.get_mass_reading()[i]) + return result -class VacscanProxy(Proxy): +class EvisionProxy(Proxy, EvisionInterface): def __init__(self): super().__init__( @@ -105,8 +123,8 @@

            Module egse.vacuum.mks.vacscan

            Classes

            -
            -class VacscanCommand +
            +class EvisionCommand (name, cmd, response=None, wait=None, check=None, description=None, device_method=None)
            @@ -120,7 +138,7 @@

            Classes

            Expand source code -
            class VacscanCommand(ClientServerCommand):
            +
            class EvisionCommand(ClientServerCommand):
                 pass

            Ancestors

            @@ -138,8 +156,8 @@

            Inherited members

          -
          -class VacscanProtocol +
          +class EvisionProtocol (control_server: ControlServer)
          @@ -155,7 +173,7 @@

          Inherited members

          Expand source code -
          class VacscanProtocol(CommandProtocol):
          +
          class EvisionProtocol(CommandProtocol):
           
               def __init__(self, control_server: ControlServer):
           
          @@ -163,13 +181,18 @@ 

          Inherited members

          self.control_server = control_server if Settings.simulation_mode(): - self.dev = VacscanSimulator() + self.dev = EvisionSimulator() else: - self.dev = VacscanController() + self.dev = EvisionDriver() - self.load_commands(DEVICE_SETTINGS.Commands, VacscanCommand, VacscanInterface) + self.load_commands(DEVICE_SETTINGS.Commands, EvisionCommand, EvisionInterface) self.build_device_method_lookup_table(self.dev) - + + self.dev.control_sensor() + self.dev.filament_status() + self.dev.rga_status() + + self.massreading_gauges = [Gauge(f'GSRON_EVISION_MASSREADING_{i}', '') for i in range(0, 200)] # move to parent class? def get_bind_address(self): @@ -181,14 +204,25 @@

          Inherited members

          def get_status(self): status_dict = super().get_status() - + + status_dict['ScanStatus'] = self.dev.get_scan_status() + status_dict['RGAStatus'] = self.dev.get_rga_status() + status_dict['FilamentStatus'] = self.dev.get_filament_status() + status_dict['MassReading'] = self.dev.get_mass_reading() return status_dict def get_housekeeping(self) -> dict: result = dict() result["timestamp"] = format_datetime() - + + for i in range(0, 200): + column_name = f'GSRON_EVISION_MASSREADING_{i}' + + result[column_name] = self.dev.get_mass_reading()[i] + + self.massreading_gauges[i].set(self.dev.get_mass_reading()[i]) + return result

          Ancestors

          @@ -218,8 +252,8 @@

          Inherited members

        -
        -class VacscanProxy +
        +class EvisionProxy

        A Proxy object will forward CommandExecutions to the connected control server @@ -236,7 +270,7 @@

        Inherited members

        Expand source code -
        class VacscanProxy(Proxy):
        +
        class EvisionProxy(Proxy, EvisionInterface):
         
             def __init__(self):
                 super().__init__(
        @@ -248,6 +282,10 @@ 

        Ancestors

      • Proxy
      • BaseProxy
      • ControlServerConnectionInterface
      • +
      • EvisionInterface
      • +
      • DeviceInterface
      • +
      • DeviceConnectionInterface
      • +
      • DeviceConnectionObservable

      Inherited members

        @@ -270,6 +308,17 @@

        Inherited members

      • send
    • +
    • EvisionInterface: + +
    @@ -289,13 +338,13 @@

    Index

  • Classes

  • diff --git a/docs/api/egse/tempcontrol/spid/spid_cs.html b/docs/api/egse/vacuum/mks/evision_cs.html similarity index 71% rename from docs/api/egse/tempcontrol/spid/spid_cs.html rename to docs/api/egse/vacuum/mks/evision_cs.html index ddedd5d..007762e 100644 --- a/docs/api/egse/tempcontrol/spid/spid_cs.html +++ b/docs/api/egse/vacuum/mks/evision_cs.html @@ -4,7 +4,7 @@ -egse.tempcontrol.spid.spid_cs API documentation +egse.vacuum.mks.evision_cs API documentation @@ -19,15 +19,14 @@
    -

    Module egse.tempcontrol.spid.spid_cs

    +

    Module egse.vacuum.mks.evision_cs

    Expand source code -
    #!/usr/bin/env python3
    -import click
    +
    import click
     import logging
     import sys
     
    @@ -36,28 +35,30 @@ 

    Module egse.tempcontrol.spid.spid_cs

    from egse.control import ControlServer from egse.settings import Settings -from egse.tempcontrol.spid.spid import PidProxy -from egse.tempcontrol.spid.spid_protocol import PidProtocol +from egse.vacuum.mks.evision import EvisionProtocol, EvisionProxy logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL) -logger = logging.getLogger('PID') +logger = logging.getLogger(__name__) -CTRL_SETTINGS = Settings.load("SPID Control Server") +CTRL_SETTINGS = Settings.load("MKS E-Vision RGA Control Server") -class PidControlServer(ControlServer): +class EvisionControlServer(ControlServer): def __init__(self): super().__init__() - self.device_protocol = PidProtocol(self) + self.device_protocol = EvisionProtocol(self) - self.logger.debug(f'Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}') + self.logger.debug(f"Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}") self.device_protocol.bind(self.dev_ctrl_cmd_sock) self.poller.register(self.dev_ctrl_cmd_sock, zmq.POLLIN) + + self.set_hk_delay(5) + self.set_delay(1) def get_communication_protocol(self): return CTRL_SETTINGS.PROTOCOL @@ -74,39 +75,38 @@

    Module egse.tempcontrol.spid.spid_cs

    def get_storage_mnemonic(self): try: return CTRL_SETTINGS.STORAGE_MNEMONIC - except: - return 'PID' + except AttributeError: + return "EVISION" def before_serve(self): start_http_server(CTRL_SETTINGS.METRICS_PORT) - @click.group() def cli(): pass @cli.command() -@click.option("--simulator", "--sim", is_flag=True, help="Start the SPID Simulator as the backend.") +@click.option("--simulator", "--sim", is_flag=True, help="Start the Evision Simulator as the backend.") def start(simulator): - """Start the SPID Control Server.""" + """Start the MKS E-vision RGA Control Server.""" if simulator: Settings.set_simulation_mode(True) try: - control_server = PidControlServer() - control_server.serve() + controller = EvisionControlServer() + controller.serve() except KeyboardInterrupt: - logger.info("Shutdown requested...exiting") + print("Shutdown requested...exiting") except SystemExit as exit_code: - logger.info("System Exit with code {}.".format(exit_code)) + print("System Exit with code {}.".format(exit_code)) sys.exit(exit_code) except Exception: - logger.exception("Cannot start the SPID Control Server") + logger.exception("Cannot start the Evision RGA Control Server") # The above line does exactly the same as the traceback, but on the logger # import traceback # traceback.print_exc(file=sys.stdout) @@ -118,7 +118,7 @@

    Module egse.tempcontrol.spid.spid_cs

    def stop(): """Send a 'quit_server' command to the Control Server.""" - with PidProxy() as proxy: + with EvisionProxy() as proxy: sp = proxy.get_service_proxy() sp.quit_server() @@ -126,42 +126,7 @@

    Module egse.tempcontrol.spid.spid_cs

    if __name__ == "__main__": - sys.exit(cli()) - - -# def parse_arguments(): -# """ Prepare the arguments that are specific for this application. """ -# parser = argparse.ArgumentParser() -# parser.add_argument('--simulator', default=False, action='store_true', -# help='Connect with the PID simulator instead of the real hardware.') -# parser.add_argument('--profile', default=False, action='store_true', -# help='Enable info logging messages with method profile information.') -# args = parser.parse_args() -# return args -# -# if __name__ == '__main__': -# -# args = parse_arguments() -# -# if args.profile: -# Settings.set_profiling(True) -# -# if args.simulator: -# Settings.set_simulation_mode(True) -# -# try: -# control_server = PidControlServer() -# control_server.serve() -# -# except KeyboardInterrupt: -# logger.info('Shutdown requested...exiting') -# except SystemExit as exit_code: -# logger.info('System exit with code {}'.format(exit_code)) -# sys.exit(exit_code) -# except Exception: -# import traceback -# traceback.print_exc(file=sys.stdout) -# sys.exit(0)
    + sys.exit(cli())
    @@ -173,8 +138,8 @@

    Module egse.tempcontrol.spid.spid_cs

    Classes

    -
    -class PidControlServer +
    +class EvisionControlServer

    The base class for all device control servers and for the Storage Manager and Configuration @@ -190,18 +155,21 @@

    Classes

    Expand source code -
    class PidControlServer(ControlServer):
    +
    class EvisionControlServer(ControlServer):
     
         def __init__(self):
             super().__init__()
     
    -        self.device_protocol = PidProtocol(self)
    +        self.device_protocol = EvisionProtocol(self)
     
    -        self.logger.debug(f'Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}')
    +        self.logger.debug(f"Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}")
     
             self.device_protocol.bind(self.dev_ctrl_cmd_sock)
     
             self.poller.register(self.dev_ctrl_cmd_sock, zmq.POLLIN)
    +        
    +        self.set_hk_delay(5)
    +        self.set_delay(1)
     
         def get_communication_protocol(self):
             return CTRL_SETTINGS.PROTOCOL
    @@ -218,8 +186,8 @@ 

    Classes

    def get_storage_mnemonic(self): try: return CTRL_SETTINGS.STORAGE_MNEMONIC - except: - return 'PID' + except AttributeError: + return "EVISION" def before_serve(self): start_http_server(CTRL_SETTINGS.METRICS_PORT)
    @@ -230,7 +198,7 @@

    Ancestors

    Methods

    -
    +
    def before_serve(self)
    @@ -243,7 +211,7 @@

    Methods

    start_http_server(CTRL_SETTINGS.METRICS_PORT)
    -
    +
    def get_commanding_port(self)
    @@ -256,7 +224,7 @@

    Methods

    return CTRL_SETTINGS.COMMANDING_PORT
    -
    +
    def get_communication_protocol(self)
    @@ -269,7 +237,7 @@

    Methods

    return CTRL_SETTINGS.PROTOCOL
    -
    +
    def get_monitoring_port(self)
    @@ -282,7 +250,7 @@

    Methods

    return CTRL_SETTINGS.MONITORING_PORT
    -
    +
    def get_service_port(self)
    @@ -295,7 +263,7 @@

    Methods

    return CTRL_SETTINGS.SERVICE_PORT
    -
    +
    def get_storage_mnemonic(self)
    @@ -307,8 +275,8 @@

    Methods

    def get_storage_mnemonic(self):
         try:
             return CTRL_SETTINGS.STORAGE_MNEMONIC
    -    except:
    -        return 'PID'
    + except AttributeError: + return "EVISION"
    @@ -316,10 +284,15 @@

    Inherited members

    • ControlServer:
    • @@ -336,20 +309,20 @@

      Index

      • Super-module

      • Classes

        diff --git a/docs/api/egse/vacuum/mks/evision_devif.html b/docs/api/egse/vacuum/mks/evision_devif.html new file mode 100644 index 0000000..3abeeb4 --- /dev/null +++ b/docs/api/egse/vacuum/mks/evision_devif.html @@ -0,0 +1,1240 @@ + + + + + + +egse.vacuum.mks.evision_devif API documentation + + + + + + + + + + + +
        +
        +
        +

        Module egse.vacuum.mks.evision_devif

        +
        +
        +
        + +Expand source code + +
        import logging
        +from threading import Thread
        +
        +import time
        +
        +from egse.socketdevice import SocketDevice
        +from egse.vacuum.mks.evision_interface import EvisionInterface
        +
        +logger = logging.getLogger(__name__)
        +
        +class EvisionException(Exception):
        +    pass
        +
        +class EvisionDriver(SocketDevice, EvisionInterface):
        +    def __init__(self, host=None, port=None):
        +        self._host = '192.168.1.70' if host == None else host
        +        self._port = 10014          if port == None else port
        +        
        +        super().__init__(hostname=self._host, port=self._port, timeout=2)
        +        self.connect()
        +        
        +        self.filament_num = 0
        +        
        +        self.filament = {
        +            'SummaryState' : None,
        +            'ActiveFilament' : None,
        +            'ExternalTripEnable' : None,
        +            'ExternalTripMode' : None,
        +            'EmissionTripEnable': None,
        +            'MaxOnTime' : None,
        +            'OnTimeRemaining' : None,
        +            'Trip' : None,
        +            'Drive' : None,
        +            'EmissionTripState' : None,
        +            'ExternalTripState' : None,
        +            'RVCTripState' : None,
        +            'GaugeTripState' : None
        +        }
        +        
        +        self.status = {
        +            'SerialNumber' : None,
        +            'Name' : None,
        +            'State' : None,
        +            'UserApplication' : None,
        +            'UserAddress' : None,
        +            'ProductID' : None,
        +            'DetectorTType' : None,
        +            'TotalPressureGauge' : None,
        +            'FilamentType' : None,
        +            'SensorType' : None,
        +            'MaxMass' : None,
        +            'ActiveFilament' : None
        +        }
        +        
        +        self.current_scan = {
        +            'Name' : None,
        +            'StartMass' : None,
        +            'EndMass' : None,
        +            'FilterMode' : None,
        +            'Accuracy' : None,
        +            'EGainIndex' : None,
        +            'SourceIndex' : None,
        +            'DetectorIndex' : None,
        +            'Running': False
        +        }
        +        
        +        self.mass_reading = [0] * 200
        +        self.zero_reading = None
        +        
        +        self.thread = Thread(target=self.reader_thread, daemon=True)
        +        self.thread.start()
        +                
        +    def reader_thread(self):
        +        while True:
        +            try:
        +                responses = self.wait_for_response().split(b'\r\n\r\r')
        +            except:
        +                # logger.info("No response")
        +                pass
        +            else:
        +                try:
        +                    for response in responses:
        +                        if b'Control' in response:
        +                            if b'OK' in response:
        +                                pass
        +                            else:
        +                                logger.warning("Could not control sensor")  
        +                                
        +                        if b'Release' in response:
        +                            if b'OK' in response:
        +                                pass
        +                            else:
        +                                logger.warning("Could not release sensor")
        +                                
        +                        elif b'FilamentSelect' in response:
        +                            response = response.decode()
        +                            response = response.split('\r\n')
        +                            
        +                            self.filament_num                   = response[1].split()[-1]
        +                            
        +                        elif b'FilamentControl' in response:
        +                            response = response.decode()
        +                            response = response.split('\r\n')
        +                        
        +                        elif b'FilamentStatus' in response:
        +                            response = response.decode()
        +                            response = response.split('\r\n')
        +                            
        +                            self.filament_num                   = response[0].split()[1]
        +                            self.filament['SummaryState']       = response[0].split()[-1]
        +                            self.filament['Trip']               = response[1].split()[-1]
        +                            self.filament['Drive']              = response[2].split()[-1]
        +                            self.filament['EmissionTripEnable'] = response[3].split()[-1]
        +                            self.filament['ExternalTripState']  = response[4].split()[-1]
        +                            self.filament['RVCTripState']       = response[5].split()[-1]
        +                            self.filament['GaugeTripState']     = response[6].split()[-1]
        +                        
        +                        elif b'FilamentTimeRemaining' in response:
        +                            response.decode()
        +                            print(f"Filament time remaining: {response}")
        +                            self.filament['OnTimeRemaining'] = response[-1]
        +                        
        +                        elif b'FilamentInfo' in response:
        +                            response = response.decode()
        +                            response = response.split('\r\n')
        +
        +                            self.filament['SummaryState']       = response[1].split()[-1]
        +                            self.filament['ActiveFilament']     = response[2].split()[-1]
        +                            self.filament['ExternalTripEnable'] = response[3].split()[-1]
        +                            self.filament['ExternalTripMode']   = response[4].split()[-1]
        +                            self.filament['EmissionTripEnable'] = response[5].split()[-1]
        +                            self.filament['MaxOnTime']          = response[6].split()[-1]
        +                            self.filament['OnTimeRemaining']    = response[7].split()[-1]
        +                            self.filament['Trip']               = response[8].split()[-1]
        +                            self.filament['Drive']              = response[9].split()[-1]
        +                            self.filament['EmissionTripState']  = response[10].split()[-1]
        +                            self.filament['ExternalTripState']  = response[11].split()[-1]
        +                            self.filament['RVCTripState']       = response[12].split()[-1]
        +                            self.filament['GaugeTripState']     = response[13].split()[-1]
        +                            
        +                        elif b'Info  OK' in response:
        +                            response = response.decode()
        +                            response = response.split('\r\n') 
        +
        +                            self.status = {
        +                                'SerialNumber'          : response[1].split()[-1],
        +                                'Name'                  : response[2].split()[-1],
        +                                'State'                 : response[3].split()[-1],
        +                                'UserApplication'       : response[4].split()[-1],
        +                                'UserAddress'           : response[6].split()[-1],
        +                                'ProductID'             : ' '.join(response[7].split()[2:]),
        +                                'DetectorType'          : ''.join(response[9].split()[2:]),
        +                                'TotalPressureGauge'    : ' '.join(response[12].split()[2:]),
        +                                'FilamentType'          : response[13].split()[-1],
        +                                'SensorType'            : ' '.join(response[15].split()[2:]),
        +                                'MaxMass'               : response[24].split()[-1],
        +                                'ActiveFilament'        : response[25].split()[-1]
        +                            }
        +                            
        +                        elif b'AddBarchart' in response:
        +                            response = response.decode()
        +                            response = response.split('\r\n')
        +                            
        +                            self.current_scan = {
        +                                'Name'          : response[1].split()[-1],
        +                                'StartMass'     : response[2].split()[-1],
        +                                'EndMass'       : response[3].split()[-1],
        +                                'FilterMode'    : response[4].split()[-1],
        +                                'Accuracy'      : response[5].split()[-1],
        +                                'EGainIndex'    : response[6].split()[-1],
        +                                'SourceIndex'   : response[7].split()[-1],
        +                                'DetectorIndex' : response[8].split()[-1],
        +                                'Running'       : False
        +                            }
        +
        +                        elif b'StartingScan' in response:
        +                            self.current_scan['Running'] = True
        +        
        +                        elif b'ScanAdd' in response:
        +                            if b'OK' in response:
        +                                pass
        +                            else:
        +                                logger.warning("Could not add scan")
        +                        
        +                        elif b'ScanStart' in response:
        +                            if b'OK' in response:
        +                                pass
        +                            else:
        +                                logger.warning("Could not start scan")
        +                        
        +                        elif b'StartingMeasurement' in response:
        +                            response = response.decode()
        +                            self.current_scan['Name'] = response.split()[-1]
        +                            
        +                        elif b'MassReading' in response:
        +                            response = response.decode()
        +                            
        +                            mass_num = response.split()[1]
        +                            mass     = response.split()[-1]
        +                            
        +                            self.mass_reading[int(mass_num)] = float(mass)
        +                        
        +                        elif b'ZeroReading' in response:
        +                            response = response.decode()
        +                            
        +                            mass = response.split()[-1]
        +                            
        +                            self.zero_reading = mass
        +                            self.mass_reading = [0] * 200
        +                except Exception as ex:
        +                    logger.warning("Received invalid response from RGA")
        +
        +    
        +    def control_sensor(self):
        +        self.send_command('Control "Common-EGSE" "1.0"\r\n')
        +    
        +    def release_sensor(self):
        +        self.send_command("Release\r\n")
        +    
        +    def filament_status(self):
        +        self.send_command("FilamentInfo\r\n")    
        +    
        +    def rga_status(self):
        +        self.send_command("Info\r\n")
        +    
        +    def filament_select(self, num):
        +        self.send_command(f"FilamentSelect {num}\r\n")
        +
        +    def filament_control(self, state):
        +        control = 'On' if state else 'Off'
        +        self.send_command(f"FilamentControl {control}\r\n")
        +        
        +    def add_bar_chart(self, name, startMass=1, endMass=100, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +        filterMode = 'PeakCenter' if filterMode == 'Peak center' else filterMode
        +        self.send_command(f"AddBarchart {name} {startMass} {endMass} {filterMode} {accuracy} {eGainIndex} {sourceIndex} {detectorIndex}\r\n")
        +    
        +    def add_single_peak(self, name, mass=4, accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +        self.send_command(f"AddSinglePeak {name} {mass} {accuracy} {eGainIndex} {sourceIndex} {detectorIndex}\r\n")
        +    
        +    def add_peak_jump(self, name, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +        self.send_command(f"AddPeakJump {name} {filterMode} {accuracy} {eGainIndex} {sourceIndex} {detectorIndex}\r\n")
        +    
        +    def add_analog(self, name, startMass=1, endMass=50, pointsPerPeak=32, accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +        self.send_command(f'AddAnalog {name} {startMass} {endMass} {pointsPerPeak} {accuracy} {eGainIndex} {sourceIndex} {detectorIndex}\r\n')
        +    
        +    def measurement_add_mass(self, mass):
        +        self.send_command(f'MeasurementAddMass {mass}\r\n')
        +    
        +    def measurement_remove_mass(self, mass):
        +        self.send_command(f"MeasurementRemoveMass {mass}\r\n")
        +    
        +    def measurement_remove_all(self):
        +        self.send_command(f"MeasurementRemoveAll\r\n")
        +    
        +    def measurement_remove(self, name):
        +        self.send_command(f"MeasurementRemove {name}\r\n")
        +    
        +    def add_scan(self, name):
        +        self.send_command(f'ScanAdd {name}\r\n')
        +    
        +    def start_scan(self, num):
        +        self.send_command(f'ScanStart {num}\r\n')
        +        
        +    def stop_scan(self):
        +        self.send_command(f'ScanStop\r\n')
        +        
        +    def resume_scan(self, num):
        +        self.send_command(f'ScanResume {num}\r\n')
        +        
        +    def restart_scan(self):
        +        self.send_command(f'ScanRestart\r\n')
        +        
        +    def get_mass_reading(self):
        +        return self.mass_reading
        +    
        +    def get_filament_status(self):
        +        return self.filament
        +    
        +    def get_rga_status(self):
        +        return self.status
        +    
        +    def get_scan_status(self):
        +        return self.current_scan
        +    
        +def main():
        +    dev = EvisionDriver()
        +    
        +    dev.control_sensor()
        +    
        +    dev.filament_select(1)
        +    dev.filament_control(True)
        +    
        +    while dev.filament['SummaryState'] == 'On':
        +        time.sleep(1)
        +    
        +    dev.add_bar_chart(name='Bar', startMass=1, endMass=10, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0)
        +    dev.add_scan(name='bar')
        +    dev.start_scan(1)
        +    
        +    time.sleep(5)
        +    
        +    print(dev.mass_reading)
        +    print(dev.zero_reading)
        +    # dev.filament_status()
        +    
        +    
        +    # time.sleep(2)
        +    
        +    # print(dev.filament_num)
        +    # print(dev.filament)
        +    
        +if __name__ == "__main__":
        +    main()
        +
        +
        +
        +
        +
        +
        +
        +

        Functions

        +
        +
        +def main() +
        +
        +
        +
        + +Expand source code + +
        def main():
        +    dev = EvisionDriver()
        +    
        +    dev.control_sensor()
        +    
        +    dev.filament_select(1)
        +    dev.filament_control(True)
        +    
        +    while dev.filament['SummaryState'] == 'On':
        +        time.sleep(1)
        +    
        +    dev.add_bar_chart(name='Bar', startMass=1, endMass=10, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0)
        +    dev.add_scan(name='bar')
        +    dev.start_scan(1)
        +    
        +    time.sleep(5)
        +    
        +    print(dev.mass_reading)
        +    print(dev.zero_reading)
        +    # dev.filament_status()
        +    
        +    
        +    # time.sleep(2)
        +    
        +    # print(dev.filament_num)
        +    # print(dev.filament)
        +
        +
        +
        +
        +
        +

        Classes

        +
        +
        +class EvisionDriver +(host=None, port=None) +
        +
        +

        Generic base class for socket based device interface classes.

        +

        This base class handles the socket connection.

        +
        + +Expand source code + +
        class EvisionDriver(SocketDevice, EvisionInterface):
        +    def __init__(self, host=None, port=None):
        +        self._host = '192.168.1.70' if host == None else host
        +        self._port = 10014          if port == None else port
        +        
        +        super().__init__(hostname=self._host, port=self._port, timeout=2)
        +        self.connect()
        +        
        +        self.filament_num = 0
        +        
        +        self.filament = {
        +            'SummaryState' : None,
        +            'ActiveFilament' : None,
        +            'ExternalTripEnable' : None,
        +            'ExternalTripMode' : None,
        +            'EmissionTripEnable': None,
        +            'MaxOnTime' : None,
        +            'OnTimeRemaining' : None,
        +            'Trip' : None,
        +            'Drive' : None,
        +            'EmissionTripState' : None,
        +            'ExternalTripState' : None,
        +            'RVCTripState' : None,
        +            'GaugeTripState' : None
        +        }
        +        
        +        self.status = {
        +            'SerialNumber' : None,
        +            'Name' : None,
        +            'State' : None,
        +            'UserApplication' : None,
        +            'UserAddress' : None,
        +            'ProductID' : None,
        +            'DetectorTType' : None,
        +            'TotalPressureGauge' : None,
        +            'FilamentType' : None,
        +            'SensorType' : None,
        +            'MaxMass' : None,
        +            'ActiveFilament' : None
        +        }
        +        
        +        self.current_scan = {
        +            'Name' : None,
        +            'StartMass' : None,
        +            'EndMass' : None,
        +            'FilterMode' : None,
        +            'Accuracy' : None,
        +            'EGainIndex' : None,
        +            'SourceIndex' : None,
        +            'DetectorIndex' : None,
        +            'Running': False
        +        }
        +        
        +        self.mass_reading = [0] * 200
        +        self.zero_reading = None
        +        
        +        self.thread = Thread(target=self.reader_thread, daemon=True)
        +        self.thread.start()
        +                
        +    def reader_thread(self):
        +        while True:
        +            try:
        +                responses = self.wait_for_response().split(b'\r\n\r\r')
        +            except:
        +                # logger.info("No response")
        +                pass
        +            else:
        +                try:
        +                    for response in responses:
        +                        if b'Control' in response:
        +                            if b'OK' in response:
        +                                pass
        +                            else:
        +                                logger.warning("Could not control sensor")  
        +                                
        +                        if b'Release' in response:
        +                            if b'OK' in response:
        +                                pass
        +                            else:
        +                                logger.warning("Could not release sensor")
        +                                
        +                        elif b'FilamentSelect' in response:
        +                            response = response.decode()
        +                            response = response.split('\r\n')
        +                            
        +                            self.filament_num                   = response[1].split()[-1]
        +                            
        +                        elif b'FilamentControl' in response:
        +                            response = response.decode()
        +                            response = response.split('\r\n')
        +                        
        +                        elif b'FilamentStatus' in response:
        +                            response = response.decode()
        +                            response = response.split('\r\n')
        +                            
        +                            self.filament_num                   = response[0].split()[1]
        +                            self.filament['SummaryState']       = response[0].split()[-1]
        +                            self.filament['Trip']               = response[1].split()[-1]
        +                            self.filament['Drive']              = response[2].split()[-1]
        +                            self.filament['EmissionTripEnable'] = response[3].split()[-1]
        +                            self.filament['ExternalTripState']  = response[4].split()[-1]
        +                            self.filament['RVCTripState']       = response[5].split()[-1]
        +                            self.filament['GaugeTripState']     = response[6].split()[-1]
        +                        
        +                        elif b'FilamentTimeRemaining' in response:
        +                            response.decode()
        +                            print(f"Filament time remaining: {response}")
        +                            self.filament['OnTimeRemaining'] = response[-1]
        +                        
        +                        elif b'FilamentInfo' in response:
        +                            response = response.decode()
        +                            response = response.split('\r\n')
        +
        +                            self.filament['SummaryState']       = response[1].split()[-1]
        +                            self.filament['ActiveFilament']     = response[2].split()[-1]
        +                            self.filament['ExternalTripEnable'] = response[3].split()[-1]
        +                            self.filament['ExternalTripMode']   = response[4].split()[-1]
        +                            self.filament['EmissionTripEnable'] = response[5].split()[-1]
        +                            self.filament['MaxOnTime']          = response[6].split()[-1]
        +                            self.filament['OnTimeRemaining']    = response[7].split()[-1]
        +                            self.filament['Trip']               = response[8].split()[-1]
        +                            self.filament['Drive']              = response[9].split()[-1]
        +                            self.filament['EmissionTripState']  = response[10].split()[-1]
        +                            self.filament['ExternalTripState']  = response[11].split()[-1]
        +                            self.filament['RVCTripState']       = response[12].split()[-1]
        +                            self.filament['GaugeTripState']     = response[13].split()[-1]
        +                            
        +                        elif b'Info  OK' in response:
        +                            response = response.decode()
        +                            response = response.split('\r\n') 
        +
        +                            self.status = {
        +                                'SerialNumber'          : response[1].split()[-1],
        +                                'Name'                  : response[2].split()[-1],
        +                                'State'                 : response[3].split()[-1],
        +                                'UserApplication'       : response[4].split()[-1],
        +                                'UserAddress'           : response[6].split()[-1],
        +                                'ProductID'             : ' '.join(response[7].split()[2:]),
        +                                'DetectorType'          : ''.join(response[9].split()[2:]),
        +                                'TotalPressureGauge'    : ' '.join(response[12].split()[2:]),
        +                                'FilamentType'          : response[13].split()[-1],
        +                                'SensorType'            : ' '.join(response[15].split()[2:]),
        +                                'MaxMass'               : response[24].split()[-1],
        +                                'ActiveFilament'        : response[25].split()[-1]
        +                            }
        +                            
        +                        elif b'AddBarchart' in response:
        +                            response = response.decode()
        +                            response = response.split('\r\n')
        +                            
        +                            self.current_scan = {
        +                                'Name'          : response[1].split()[-1],
        +                                'StartMass'     : response[2].split()[-1],
        +                                'EndMass'       : response[3].split()[-1],
        +                                'FilterMode'    : response[4].split()[-1],
        +                                'Accuracy'      : response[5].split()[-1],
        +                                'EGainIndex'    : response[6].split()[-1],
        +                                'SourceIndex'   : response[7].split()[-1],
        +                                'DetectorIndex' : response[8].split()[-1],
        +                                'Running'       : False
        +                            }
        +
        +                        elif b'StartingScan' in response:
        +                            self.current_scan['Running'] = True
        +        
        +                        elif b'ScanAdd' in response:
        +                            if b'OK' in response:
        +                                pass
        +                            else:
        +                                logger.warning("Could not add scan")
        +                        
        +                        elif b'ScanStart' in response:
        +                            if b'OK' in response:
        +                                pass
        +                            else:
        +                                logger.warning("Could not start scan")
        +                        
        +                        elif b'StartingMeasurement' in response:
        +                            response = response.decode()
        +                            self.current_scan['Name'] = response.split()[-1]
        +                            
        +                        elif b'MassReading' in response:
        +                            response = response.decode()
        +                            
        +                            mass_num = response.split()[1]
        +                            mass     = response.split()[-1]
        +                            
        +                            self.mass_reading[int(mass_num)] = float(mass)
        +                        
        +                        elif b'ZeroReading' in response:
        +                            response = response.decode()
        +                            
        +                            mass = response.split()[-1]
        +                            
        +                            self.zero_reading = mass
        +                            self.mass_reading = [0] * 200
        +                except Exception as ex:
        +                    logger.warning("Received invalid response from RGA")
        +
        +    
        +    def control_sensor(self):
        +        self.send_command('Control "Common-EGSE" "1.0"\r\n')
        +    
        +    def release_sensor(self):
        +        self.send_command("Release\r\n")
        +    
        +    def filament_status(self):
        +        self.send_command("FilamentInfo\r\n")    
        +    
        +    def rga_status(self):
        +        self.send_command("Info\r\n")
        +    
        +    def filament_select(self, num):
        +        self.send_command(f"FilamentSelect {num}\r\n")
        +
        +    def filament_control(self, state):
        +        control = 'On' if state else 'Off'
        +        self.send_command(f"FilamentControl {control}\r\n")
        +        
        +    def add_bar_chart(self, name, startMass=1, endMass=100, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +        filterMode = 'PeakCenter' if filterMode == 'Peak center' else filterMode
        +        self.send_command(f"AddBarchart {name} {startMass} {endMass} {filterMode} {accuracy} {eGainIndex} {sourceIndex} {detectorIndex}\r\n")
        +    
        +    def add_single_peak(self, name, mass=4, accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +        self.send_command(f"AddSinglePeak {name} {mass} {accuracy} {eGainIndex} {sourceIndex} {detectorIndex}\r\n")
        +    
        +    def add_peak_jump(self, name, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +        self.send_command(f"AddPeakJump {name} {filterMode} {accuracy} {eGainIndex} {sourceIndex} {detectorIndex}\r\n")
        +    
        +    def add_analog(self, name, startMass=1, endMass=50, pointsPerPeak=32, accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +        self.send_command(f'AddAnalog {name} {startMass} {endMass} {pointsPerPeak} {accuracy} {eGainIndex} {sourceIndex} {detectorIndex}\r\n')
        +    
        +    def measurement_add_mass(self, mass):
        +        self.send_command(f'MeasurementAddMass {mass}\r\n')
        +    
        +    def measurement_remove_mass(self, mass):
        +        self.send_command(f"MeasurementRemoveMass {mass}\r\n")
        +    
        +    def measurement_remove_all(self):
        +        self.send_command(f"MeasurementRemoveAll\r\n")
        +    
        +    def measurement_remove(self, name):
        +        self.send_command(f"MeasurementRemove {name}\r\n")
        +    
        +    def add_scan(self, name):
        +        self.send_command(f'ScanAdd {name}\r\n')
        +    
        +    def start_scan(self, num):
        +        self.send_command(f'ScanStart {num}\r\n')
        +        
        +    def stop_scan(self):
        +        self.send_command(f'ScanStop\r\n')
        +        
        +    def resume_scan(self, num):
        +        self.send_command(f'ScanResume {num}\r\n')
        +        
        +    def restart_scan(self):
        +        self.send_command(f'ScanRestart\r\n')
        +        
        +    def get_mass_reading(self):
        +        return self.mass_reading
        +    
        +    def get_filament_status(self):
        +        return self.filament
        +    
        +    def get_rga_status(self):
        +        return self.status
        +    
        +    def get_scan_status(self):
        +        return self.current_scan
        +
        +

        Ancestors

        + +

        Methods

        +
        +
        +def add_analog(self, name, startMass=1, endMass=50, pointsPerPeak=32, accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0) +
        +
        +
        +
        + +Expand source code + +
        def add_analog(self, name, startMass=1, endMass=50, pointsPerPeak=32, accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +    self.send_command(f'AddAnalog {name} {startMass} {endMass} {pointsPerPeak} {accuracy} {eGainIndex} {sourceIndex} {detectorIndex}\r\n')
        +
        +
        +
        +def add_bar_chart(self, name, startMass=1, endMass=100, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0) +
        +
        +
        +
        + +Expand source code + +
        def add_bar_chart(self, name, startMass=1, endMass=100, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +    filterMode = 'PeakCenter' if filterMode == 'Peak center' else filterMode
        +    self.send_command(f"AddBarchart {name} {startMass} {endMass} {filterMode} {accuracy} {eGainIndex} {sourceIndex} {detectorIndex}\r\n")
        +
        +
        +
        +def add_peak_jump(self, name, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0) +
        +
        +
        +
        + +Expand source code + +
        def add_peak_jump(self, name, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +    self.send_command(f"AddPeakJump {name} {filterMode} {accuracy} {eGainIndex} {sourceIndex} {detectorIndex}\r\n")
        +
        +
        +
        +def add_scan(self, name) +
        +
        +
        +
        + +Expand source code + +
        def add_scan(self, name):
        +    self.send_command(f'ScanAdd {name}\r\n')
        +
        +
        +
        +def add_single_peak(self, name, mass=4, accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0) +
        +
        +
        +
        + +Expand source code + +
        def add_single_peak(self, name, mass=4, accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +    self.send_command(f"AddSinglePeak {name} {mass} {accuracy} {eGainIndex} {sourceIndex} {detectorIndex}\r\n")
        +
        +
        +
        +def control_sensor(self) +
        +
        +
        +
        + +Expand source code + +
        def control_sensor(self):
        +    self.send_command('Control "Common-EGSE" "1.0"\r\n')
        +
        +
        +
        +def filament_control(self, state) +
        +
        +
        +
        + +Expand source code + +
        def filament_control(self, state):
        +    control = 'On' if state else 'Off'
        +    self.send_command(f"FilamentControl {control}\r\n")
        +
        +
        +
        +def filament_select(self, num) +
        +
        +
        +
        + +Expand source code + +
        def filament_select(self, num):
        +    self.send_command(f"FilamentSelect {num}\r\n")
        +
        +
        +
        +def filament_status(self) +
        +
        +
        +
        + +Expand source code + +
        def filament_status(self):
        +    self.send_command("FilamentInfo\r\n")    
        +
        +
        +
        +def get_filament_status(self) +
        +
        +
        +
        + +Expand source code + +
        def get_filament_status(self):
        +    return self.filament
        +
        +
        +
        +def get_mass_reading(self) +
        +
        +
        +
        + +Expand source code + +
        def get_mass_reading(self):
        +    return self.mass_reading
        +
        +
        +
        +def get_rga_status(self) +
        +
        +
        +
        + +Expand source code + +
        def get_rga_status(self):
        +    return self.status
        +
        +
        +
        +def get_scan_status(self) +
        +
        +
        +
        + +Expand source code + +
        def get_scan_status(self):
        +    return self.current_scan
        +
        +
        +
        +def measurement_add_mass(self, mass) +
        +
        +
        +
        + +Expand source code + +
        def measurement_add_mass(self, mass):
        +    self.send_command(f'MeasurementAddMass {mass}\r\n')
        +
        +
        +
        +def measurement_remove(self, name) +
        +
        +
        +
        + +Expand source code + +
        def measurement_remove(self, name):
        +    self.send_command(f"MeasurementRemove {name}\r\n")
        +
        +
        +
        +def measurement_remove_all(self) +
        +
        +
        +
        + +Expand source code + +
        def measurement_remove_all(self):
        +    self.send_command(f"MeasurementRemoveAll\r\n")
        +
        +
        +
        +def measurement_remove_mass(self, mass) +
        +
        +
        +
        + +Expand source code + +
        def measurement_remove_mass(self, mass):
        +    self.send_command(f"MeasurementRemoveMass {mass}\r\n")
        +
        +
        +
        +def reader_thread(self) +
        +
        +
        +
        + +Expand source code + +
        def reader_thread(self):
        +    while True:
        +        try:
        +            responses = self.wait_for_response().split(b'\r\n\r\r')
        +        except:
        +            # logger.info("No response")
        +            pass
        +        else:
        +            try:
        +                for response in responses:
        +                    if b'Control' in response:
        +                        if b'OK' in response:
        +                            pass
        +                        else:
        +                            logger.warning("Could not control sensor")  
        +                            
        +                    if b'Release' in response:
        +                        if b'OK' in response:
        +                            pass
        +                        else:
        +                            logger.warning("Could not release sensor")
        +                            
        +                    elif b'FilamentSelect' in response:
        +                        response = response.decode()
        +                        response = response.split('\r\n')
        +                        
        +                        self.filament_num                   = response[1].split()[-1]
        +                        
        +                    elif b'FilamentControl' in response:
        +                        response = response.decode()
        +                        response = response.split('\r\n')
        +                    
        +                    elif b'FilamentStatus' in response:
        +                        response = response.decode()
        +                        response = response.split('\r\n')
        +                        
        +                        self.filament_num                   = response[0].split()[1]
        +                        self.filament['SummaryState']       = response[0].split()[-1]
        +                        self.filament['Trip']               = response[1].split()[-1]
        +                        self.filament['Drive']              = response[2].split()[-1]
        +                        self.filament['EmissionTripEnable'] = response[3].split()[-1]
        +                        self.filament['ExternalTripState']  = response[4].split()[-1]
        +                        self.filament['RVCTripState']       = response[5].split()[-1]
        +                        self.filament['GaugeTripState']     = response[6].split()[-1]
        +                    
        +                    elif b'FilamentTimeRemaining' in response:
        +                        response.decode()
        +                        print(f"Filament time remaining: {response}")
        +                        self.filament['OnTimeRemaining'] = response[-1]
        +                    
        +                    elif b'FilamentInfo' in response:
        +                        response = response.decode()
        +                        response = response.split('\r\n')
        +
        +                        self.filament['SummaryState']       = response[1].split()[-1]
        +                        self.filament['ActiveFilament']     = response[2].split()[-1]
        +                        self.filament['ExternalTripEnable'] = response[3].split()[-1]
        +                        self.filament['ExternalTripMode']   = response[4].split()[-1]
        +                        self.filament['EmissionTripEnable'] = response[5].split()[-1]
        +                        self.filament['MaxOnTime']          = response[6].split()[-1]
        +                        self.filament['OnTimeRemaining']    = response[7].split()[-1]
        +                        self.filament['Trip']               = response[8].split()[-1]
        +                        self.filament['Drive']              = response[9].split()[-1]
        +                        self.filament['EmissionTripState']  = response[10].split()[-1]
        +                        self.filament['ExternalTripState']  = response[11].split()[-1]
        +                        self.filament['RVCTripState']       = response[12].split()[-1]
        +                        self.filament['GaugeTripState']     = response[13].split()[-1]
        +                        
        +                    elif b'Info  OK' in response:
        +                        response = response.decode()
        +                        response = response.split('\r\n') 
        +
        +                        self.status = {
        +                            'SerialNumber'          : response[1].split()[-1],
        +                            'Name'                  : response[2].split()[-1],
        +                            'State'                 : response[3].split()[-1],
        +                            'UserApplication'       : response[4].split()[-1],
        +                            'UserAddress'           : response[6].split()[-1],
        +                            'ProductID'             : ' '.join(response[7].split()[2:]),
        +                            'DetectorType'          : ''.join(response[9].split()[2:]),
        +                            'TotalPressureGauge'    : ' '.join(response[12].split()[2:]),
        +                            'FilamentType'          : response[13].split()[-1],
        +                            'SensorType'            : ' '.join(response[15].split()[2:]),
        +                            'MaxMass'               : response[24].split()[-1],
        +                            'ActiveFilament'        : response[25].split()[-1]
        +                        }
        +                        
        +                    elif b'AddBarchart' in response:
        +                        response = response.decode()
        +                        response = response.split('\r\n')
        +                        
        +                        self.current_scan = {
        +                            'Name'          : response[1].split()[-1],
        +                            'StartMass'     : response[2].split()[-1],
        +                            'EndMass'       : response[3].split()[-1],
        +                            'FilterMode'    : response[4].split()[-1],
        +                            'Accuracy'      : response[5].split()[-1],
        +                            'EGainIndex'    : response[6].split()[-1],
        +                            'SourceIndex'   : response[7].split()[-1],
        +                            'DetectorIndex' : response[8].split()[-1],
        +                            'Running'       : False
        +                        }
        +
        +                    elif b'StartingScan' in response:
        +                        self.current_scan['Running'] = True
        +    
        +                    elif b'ScanAdd' in response:
        +                        if b'OK' in response:
        +                            pass
        +                        else:
        +                            logger.warning("Could not add scan")
        +                    
        +                    elif b'ScanStart' in response:
        +                        if b'OK' in response:
        +                            pass
        +                        else:
        +                            logger.warning("Could not start scan")
        +                    
        +                    elif b'StartingMeasurement' in response:
        +                        response = response.decode()
        +                        self.current_scan['Name'] = response.split()[-1]
        +                        
        +                    elif b'MassReading' in response:
        +                        response = response.decode()
        +                        
        +                        mass_num = response.split()[1]
        +                        mass     = response.split()[-1]
        +                        
        +                        self.mass_reading[int(mass_num)] = float(mass)
        +                    
        +                    elif b'ZeroReading' in response:
        +                        response = response.decode()
        +                        
        +                        mass = response.split()[-1]
        +                        
        +                        self.zero_reading = mass
        +                        self.mass_reading = [0] * 200
        +            except Exception as ex:
        +                logger.warning("Received invalid response from RGA")
        +
        +
        +
        +def release_sensor(self) +
        +
        +
        +
        + +Expand source code + +
        def release_sensor(self):
        +    self.send_command("Release\r\n")
        +
        +
        +
        +def restart_scan(self) +
        +
        +
        +
        + +Expand source code + +
        def restart_scan(self):
        +    self.send_command(f'ScanRestart\r\n')
        +
        +
        +
        +def resume_scan(self, num) +
        +
        +
        +
        + +Expand source code + +
        def resume_scan(self, num):
        +    self.send_command(f'ScanResume {num}\r\n')
        +
        +
        +
        +def rga_status(self) +
        +
        +
        +
        + +Expand source code + +
        def rga_status(self):
        +    self.send_command("Info\r\n")
        +
        +
        +
        +def start_scan(self, num) +
        +
        +
        +
        + +Expand source code + +
        def start_scan(self, num):
        +    self.send_command(f'ScanStart {num}\r\n')
        +
        +
        +
        +def stop_scan(self) +
        +
        +
        +
        + +Expand source code + +
        def stop_scan(self):
        +    self.send_command(f'ScanStop\r\n')
        +
        +
        +
        +

        Inherited members

        + +
        +
        +class EvisionException +(*args, **kwargs) +
        +
        +

        Common base class for all non-exit exceptions.

        +
        + +Expand source code + +
        class EvisionException(Exception):
        +    pass
        +
        +

        Ancestors

        +
          +
        • builtins.Exception
        • +
        • builtins.BaseException
        • +
        +
        +
        +
        +
        + +
        + + + \ No newline at end of file diff --git a/docs/api/egse/vacuum/mks/evision_interface.html b/docs/api/egse/vacuum/mks/evision_interface.html new file mode 100644 index 0000000..162521e --- /dev/null +++ b/docs/api/egse/vacuum/mks/evision_interface.html @@ -0,0 +1,439 @@ + + + + + + +egse.vacuum.mks.evision_interface API documentation + + + + + + + + + + + +
        +
        +
        +

        Module egse.vacuum.mks.evision_interface

        +
        +
        +
        + +Expand source code + +
        from egse.device import DeviceInterface
        +from egse.decorators import dynamic_interface
        +
        +
        +class EvisionError(Exception):
        +    """ Vacscan protocol errors. """
        +
        +
        +class EvisionInterface(DeviceInterface):
        +    @dynamic_interface
        +    def control_sensor(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def release_sensor(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def filament_select(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def filament_control(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def add_bar_chart(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def add_scan(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def start_scan(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def restart_scan(self):
        +        return NotImplemented
        +    
        +    @dynamic_interface
        +    def resume_scan(self):
        +        return NotImplemented
        +    
        +    @dynamic_interface
        +    def stop_scan(self):
        +        return NotImplemented
        +    
        +    @dynamic_interface
        +    def get_mass_reading(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def get_filament_status(self):
        +        return NotImplemented
        +    
        +    @dynamic_interface
        +    def measurement_remove_all(self):
        +        return NotImplemented
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +

        Classes

        +
        +
        +class EvisionError +(*args, **kwargs) +
        +
        +

        Vacscan protocol errors.

        +
        + +Expand source code + +
        class EvisionError(Exception):
        +    """ Vacscan protocol errors. """
        +
        +

        Ancestors

        +
          +
        • builtins.Exception
        • +
        • builtins.BaseException
        • +
        +
        +
        +class EvisionInterface +
        +
        +

        Generic interface for all device classes.

        +
        + +Expand source code + +
        class EvisionInterface(DeviceInterface):
        +    @dynamic_interface
        +    def control_sensor(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def release_sensor(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def filament_select(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def filament_control(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def add_bar_chart(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def add_scan(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def start_scan(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def restart_scan(self):
        +        return NotImplemented
        +    
        +    @dynamic_interface
        +    def resume_scan(self):
        +        return NotImplemented
        +    
        +    @dynamic_interface
        +    def stop_scan(self):
        +        return NotImplemented
        +    
        +    @dynamic_interface
        +    def get_mass_reading(self):
        +        return NotImplemented
        +        
        +    @dynamic_interface
        +    def get_filament_status(self):
        +        return NotImplemented
        +    
        +    @dynamic_interface
        +    def measurement_remove_all(self):
        +        return NotImplemented
        +
        +

        Ancestors

        + +

        Subclasses

        + +

        Methods

        +
        +
        +def add_bar_chart(self) +
        +
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def add_bar_chart(self):
        +    return NotImplemented
        +
        +
        +
        +def add_scan(self) +
        +
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def add_scan(self):
        +    return NotImplemented
        +
        +
        +
        +def control_sensor(self) +
        +
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def control_sensor(self):
        +    return NotImplemented
        +
        +
        +
        +def filament_control(self) +
        +
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def filament_control(self):
        +    return NotImplemented
        +
        +
        +
        +def filament_select(self) +
        +
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def filament_select(self):
        +    return NotImplemented
        +
        +
        +
        +def get_filament_status(self) +
        +
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def get_filament_status(self):
        +    return NotImplemented
        +
        +
        +
        +def get_mass_reading(self) +
        +
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def get_mass_reading(self):
        +    return NotImplemented
        +
        +
        +
        +def measurement_remove_all(self) +
        +
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def measurement_remove_all(self):
        +    return NotImplemented
        +
        +
        +
        +def release_sensor(self) +
        +
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def release_sensor(self):
        +    return NotImplemented
        +
        +
        +
        +def restart_scan(self) +
        +
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def restart_scan(self):
        +    return NotImplemented
        +
        +
        +
        +def resume_scan(self) +
        +
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def resume_scan(self):
        +    return NotImplemented
        +
        +
        +
        +def start_scan(self) +
        +
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def start_scan(self):
        +    return NotImplemented
        +
        +
        +
        +def stop_scan(self) +
        +
        +
        +
        + +Expand source code + +
        @dynamic_interface
        +def stop_scan(self):
        +    return NotImplemented
        +
        +
        +
        +

        Inherited members

        + +
        +
        +
        +
        + +
        + + + \ No newline at end of file diff --git a/docs/api/egse/vacuum/mks/vacscan_simulator.html b/docs/api/egse/vacuum/mks/evision_simulator.html similarity index 85% rename from docs/api/egse/vacuum/mks/vacscan_simulator.html rename to docs/api/egse/vacuum/mks/evision_simulator.html index a991e07..dc477fa 100644 --- a/docs/api/egse/vacuum/mks/vacscan_simulator.html +++ b/docs/api/egse/vacuum/mks/evision_simulator.html @@ -4,7 +4,7 @@ -egse.vacuum.mks.vacscan_simulator API documentation +egse.vacuum.mks.evision_simulator API documentation @@ -19,7 +19,7 @@
        -

        Module egse.vacuum.mks.vacscan_simulator

        +

        Module egse.vacuum.mks.evision_simulator

        @@ -31,15 +31,15 @@

        Module egse.vacuum.mks.vacscan_simulator

        from egse.settings import Settings from egse.simulator import Simulator from egse.command import Command -from egse.vacuum.mks.vacscan_interface import VacscanInterface +from egse.vacuum.mks.evision_interface import EvisionInterface logger = logging.getLogger(__name__) # Load the device protocol -DEVICE_PROTOCOL = Settings.load(filename='vacscan.yaml')['Commands'] +DEVICE_PROTOCOL = Settings.load(filename='evision.yaml')['Commands'] -class VacscanSimulator(VacscanInterface, Simulator): +class EvisionSimulator(EvisionInterface, Simulator): def __init__(self): super().__init__() @@ -61,8 +61,8 @@

        Module egse.vacuum.mks.vacscan_simulator

        Classes

        -
        -class VacscanSimulator +
        +class EvisionSimulator

        Generic interface for all device classes.

        @@ -70,7 +70,7 @@

        Classes

        Expand source code -
        class VacscanSimulator(VacscanInterface, Simulator):
        +
        class EvisionSimulator(EvisionInterface, Simulator):
         
             def __init__(self):
                 super().__init__()
        @@ -84,7 +84,7 @@ 

        Classes

        Ancestors

        Inherited members

        @@ -122,7 +122,7 @@

        Index

      • Classes

      • diff --git a/docs/api/egse/vacuum/mks/evision_ui.html b/docs/api/egse/vacuum/mks/evision_ui.html new file mode 100644 index 0000000..154a06c --- /dev/null +++ b/docs/api/egse/vacuum/mks/evision_ui.html @@ -0,0 +1,2200 @@ + + + + + + +egse.vacuum.mks.evision_ui API documentation + + + + + + + + + + + +
        +
        +
        +

        Module egse.vacuum.mks.evision_ui

        +
        +
        +
        + +Expand source code + +
        import argparse
        +import logging
        +import pickle
        +from datetime import datetime
        +
        +import numpy as np
        +import sys
        +import zmq
        +from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot
        +from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QVBoxLayout, QSizePolicy, QGridLayout, QGroupBox, \
        +    QLabel, QFormLayout, \
        +    QHBoxLayout, QComboBox, QSpinBox, QPushButton, \
        +    QSpacerItem, QCheckBox
        +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as Canvas
        +from matplotlib.figure import Figure
        +
        +from egse.observer import Observer, Observable
        +from egse.vacuum.mks.evision import CTRL_SETTINGS
        +from egse.vacuum.mks.evision import EvisionProxy, EvisionSimulator
        +from egse.zmq_ser import connect_address
        +
        +logger = logging.getLogger(__name__)
        +
        +class RGAMonitoringWorker(QObject):
        +
        +    rga_new_data_signal         = pyqtSignal(list)
        +    rga_new_info_signal         = pyqtSignal(dict)
        +    rga_new_filament_signal     = pyqtSignal(dict)
        +    rga_new_scan_status_signal  = pyqtSignal(dict)
        +    rga_new_running_signal      = pyqtSignal(bool)
        +    
        +    def __init__(self):
        +        """ Initialisation of a monitoring worker.
        +        
        +        This worker keeps an eye on the monitoring port of the Beaglebone rga. When a change in
        +            Relevant information occurs, a signalw ill be emitted. These signals will be used to update the GUI
        +        """
        +        
        +        
        +        super(RGAMonitoringWorker, self).__init__()
        +        
        +        self.active = False
        +        self.just_reconnected = True
        +        
        +        self.monitoring_socket = None
        +        self.is_socket_connected = True
        +        self.monitoring_timeout = 0.5
        +        
        +        self.connect_socket()
        +        
        +        # Keep track of the rga status, so we only have to send a signal when the state has changed
        +        
        +        self.previous_rga_data      = {}
        +        self.previous_rga_info      = {}
        +        self.previous_filament_info = {}
        +        self.previous_scan_reading  = {}
        +        self.previous_running       = None
        +        
        +    def connect_socket(self):
        +        """ Create a socket and connect to the monitoring port.
        +        """
        +        
        +
        +        try:
        +            transport   = CTRL_SETTINGS.PROTOCOL
        +            hostname    = CTRL_SETTINGS.HOSTNAME
        +            
        +            monitoring_port = CTRL_SETTINGS.MONITORING_PORT
        +            monitoring_address = connect_address(transport, hostname, monitoring_port)
        +            
        +            self.monitoring_socket = zmq.Context().socket(zmq.SUB)
        +            self.monitoring_socket.connect(monitoring_address)
        +            self.monitoring_socket.setsockopt_string(zmq.SUBSCRIBE, "")
        +            
        +            self.monitoring_timeout = 0.5
        +            
        +            self.is_socket_connected = True
        +            
        +        except:
        +            self.is_socket_connected = False
        +            
        +    def stop(self):
        +        
        +        """ Stop the monitoring worker.
        +
        +        The monitoring socket is disconnected from the monitoring port and is then closed immediately.
        +        """
        +        
        +        self.monitoring_socket.close()
        +        self.is_socket_connected = False
        +        
        +        self.active = False
        +        
        +    def start_process(self):
        +        """Start updated the Beaglebone status"""
        +        self.run()
        +        
        +    @pyqtSlot()
        +    def run(self):
        +        """Keep on checkin whether the Beaglebone state has changed
        +        
        +        If the beaglebone status has changed, update it in the GUI
        +
        +        Raises:
        +            Exception: ZMQ Error
        +        """
        +        
        +        self.active = True
        +        while self.is_socket_connected and self.active:
        +            
        +            try:
        +                socket_list, _, exc_list = zmq.select([self.monitoring_socket], [], [], timeout=self.monitoring_timeout)
        +
        +                if self.monitoring_socket in socket_list:
        +                    try:
        +                        pickle_string = self.monitoring_socket.recv()
        +                    except Exception:
        +                        raise Exception
        +
        +                    monitoring_info = pickle.loads(pickle_string)
        +                    
        +                    # logger.info(monitoring_info)
        +                
        +                    if 'MassReading' in monitoring_info:
        +                        mass_reading = monitoring_info['MassReading']
        +                        if mass_reading != self.previous_rga_data:
        +                            self.rga_new_data_signal.emit(mass_reading)
        +                        else:
        +                            running = False
        +                            if running != self.previous_running:
        +                                self.rga_new_running_signal.emit(running)
        +                        self.previous_rga_data = mass_reading
        +                    
        +                    if 'FilamentStatus' in monitoring_info:
        +                        info_reading = monitoring_info['FilamentStatus']
        +                        if info_reading != self.previous_filament_info:
        +                            self.rga_new_filament_signal.emit(info_reading)
        +                        self.previous_filament_info = info_reading
        +                        
        +                    if 'RGAStatus' in monitoring_info:
        +                        status_reading = monitoring_info['RGAStatus']
        +                        if status_reading != self.previous_rga_info:
        +                            self.rga_new_info_signal.emit(status_reading)
        +                        self.previous_filament_info = status_reading
        +                
        +                    if 'ScanStatus' in monitoring_info:
        +                        scan_reading = monitoring_info['ScanStatus']
        +                        if scan_reading != self.previous_scan_reading:
        +                            self.rga_new_scan_status_signal.emit(scan_reading)
        +                        self.previous_scan_reading = scan_reading
        +                
        +            except zmq.ZMQError as exc:
        +                raise exc
        +
        +class mPlotCanvas(Canvas):
        +    def __init__(self):
        +        super().__init__()
        +        
        +        # self.fig, self.ax = 
        +        self.fig = Figure()
        +        self.ax  = self.fig.add_subplot(111)
        +        Canvas.__init__(self, self.fig)
        +        Canvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding)
        +        Canvas.updateGeometry(self)
        +        
        +
        +class RGAUIView(QMainWindow, Observable):
        +    def __init__(self, *args, **kwargs):
        +        super(RGAUIView, self).__init__(*args, **kwargs)
        +        
        +        self.setWindowTitle("E-Vision Residual Gas Analyzer")
        +        self.setGeometry(300, 300, 1000, 700)
        +        self.initUI()
        +        
        +        self.rga_monitoring_thread = QThread()
        +        self.rga_monitoring_worker = RGAMonitoringWorker()
        +        self.rga_monitoring_worker.moveToThread(self.rga_monitoring_thread)
        +        
        +        self.rga_monitoring_worker.rga_new_data_signal.connect(self._on_new_data_signal)
        +        self.rga_monitoring_worker.rga_new_info_signal.connect(self._on_new_info_signal)
        +        self.rga_monitoring_worker.rga_new_filament_signal.connect(self._on_new_filament_signal)
        +        self.rga_monitoring_worker.rga_new_scan_status_signal.connect(self._on_new_scan_status_signal)
        +        self.rga_monitoring_worker.rga_new_running_signal.connect(self._on_new_running_signal)
        +        
        +        self.rga_monitoring_thread.started.connect(self.rga_monitoring_worker.start_process)
        +        
        +        self.rga_monitoring_thread.start()
        +        
        +        self.barRingBuffer = [np.nan] * 201
        +        
        +        self.single_line = [[],[]]
        +        self.peak_lines  = [{},[]]
        +        
        +    def initUI(self):
        +        self.centerWidget = QWidget(self)
        +        
        +        self.gridLayout = QGridLayout(self.centerWidget)
        +        
        +        # Filament groupbox
        +        self.groupBox   = QGroupBox()
        +        self.verticalLayout = QVBoxLayout()
        +        self.label = QLabel("<b>Filament information</b>")
        +        self.formLayout = QFormLayout()
        +        self.label_2 = QLabel("")
        +        self.label_3 = QLabel("")
        +        self.label_4 = QLabel("")
        +        self.label_5 = QLabel("")
        +        self.formLayout.addRow(QLabel("Summary state"), self.label_2)
        +        self.formLayout.addRow(QLabel("Active filament"), self.label_3)
        +        self.formLayout.addRow(QLabel("Max on time"), self.label_4)
        +        self.verticalLayout.addWidget(self.label)
        +        self.verticalLayout.addLayout(self.formLayout)
        +        self.groupBox.setLayout(self.verticalLayout)
        +        self.gridLayout.addWidget(self.groupBox, 0, 0, 1 ,1)
        +        self.groupBox.setMinimumWidth(300)
        +        self.groupBox.setMaximumWidth(400)
        +        self.groupBox.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        +        
        +        
        +        # Sensor groupbox
        +        self.groupBox_2 = QGroupBox()
        +        self.verticalLayout_2 = QVBoxLayout()
        +        self.label_6 = QLabel("<b>Sensor information</b>")
        +        self.formLayout_2 = QFormLayout()
        +        self.label_7 = QLabel("")
        +        self.label_8 = QLabel("")
        +        self.label_9 = QLabel("")
        +        self.label_10 = QLabel("")
        +        self.label_11 = QLabel("")
        +        self.label_12 = QLabel("")
        +        self.label_13 = QLabel("")
        +        self.label_14 = QLabel("")
        +        self.label_15 = QLabel("")
        +        self.label_16 = QLabel("")
        +        self.label_17 = QLabel("")
        +        self.formLayout_2.addRow(QLabel("Name"), self.label_7)
        +        self.formLayout_2.addRow(QLabel("Serial number"), self.label_8)
        +        self.formLayout_2.addRow(QLabel("User application"), self.label_9)
        +        self.formLayout_2.addRow(QLabel("User address"), self.label_10)
        +        self.formLayout_2.addRow(QLabel("State"), self.label_11)
        +        self.formLayout_2.addRow(QLabel("Product ID"), self.label_12)
        +        self.formLayout_2.addRow(QLabel("Detector type"), self.label_13)
        +        self.formLayout_2.addRow(QLabel("Total pressure gauge"), self.label_14)
        +        self.formLayout_2.addRow(QLabel("Filament type"), self.label_15)
        +        self.formLayout_2.addRow(QLabel("Sensor type"), self.label_16)
        +        self.formLayout_2.addRow(QLabel("Max mass"), self.label_17)
        +        self.verticalLayout_2.addWidget(self.label_6)
        +        self.verticalLayout_2.addLayout(self.formLayout_2)
        +        self.groupBox_2.setLayout(self.verticalLayout_2)
        +        self.gridLayout.addWidget(self.groupBox_2, 1, 0, 1 ,1)
        +        self.groupBox_2.setMinimumWidth(300)
        +        self.groupBox_2.setMaximumWidth(400)
        +        self.groupBox_2.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        +        
        +        # Scan status
        +        self.groupBox_3 = QGroupBox()
        +        self.verticalLayout_3 = QVBoxLayout()
        +        self.label_18 = QLabel("<b>Scan information</b>")
        +        self.formLayout_6 = QFormLayout()
        +        self.label_20 = QLabel("")
        +        self.label_21 = QLabel("")
        +        self.label_22 = QLabel("")
        +        self.label_23 = QLabel("")
        +        self.label_24 = QLabel("")
        +        self.label_25 = QLabel("")
        +        self.label_26 = QLabel("")
        +        self.label_27 = QLabel("")
        +        self.formLayout_6.addRow(QLabel("Start mass"), self.label_20)
        +        self.formLayout_6.addRow(QLabel("End mass"), self.label_21)
        +        self.formLayout_6.addRow(QLabel("Filter mode"), self.label_22)
        +        self.formLayout_6.addRow(QLabel("Accuracy"), self.label_23)
        +        self.formLayout_6.addRow(QLabel("E Gain Index"), self.label_24)
        +        self.formLayout_6.addRow(QLabel("Source index"), self.label_25)
        +        self.formLayout_6.addRow(QLabel("Detector index"), self.label_26)
        +        self.formLayout_6.addRow(QLabel("Running"), self.label_27)
        +        self.verticalLayout_3.addWidget(self.label_18)
        +        self.verticalLayout_3.addLayout(self.formLayout_6)
        +        self.groupBox_3.setLayout(self.verticalLayout_3)
        +        self.gridLayout.addWidget(self.groupBox_3, 2, 0 ,1, 1)
        +        self.groupBox_3.setMinimumWidth(300)
        +        self.groupBox_3.setMaximumWidth(400)
        +        self.groupBox_3.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        +        # spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        +
        +        
        +        # Plot navigation groupbox
        +        self.groupBox_4 = QGroupBox()
        +        self.horizontalLayout = QHBoxLayout()
        +        self.formLayout_3 = QFormLayout()
        +        self.label_29 = QLabel("Start mass:")
        +        self.spinBox = QSpinBox()
        +        self.spinBox.setMinimum(1)
        +        self.spinBox.setMaximum(200)
        +        self.spinBox.setValue(1)
        +        self.spinBox.setMaximumWidth(150)
        +        self.spinBox.valueChanged.connect(self._set_plot_x_axis)
        +        self.label_30 = QLabel("End mass:")
        +        self.spinBox_2 = QSpinBox()
        +        self.spinBox_2.setMinimum(2)
        +        self.spinBox_2.setMaximum(200)
        +        self.spinBox_2.setValue(100)
        +        self.spinBox_2.setMaximumWidth(150)
        +        self.spinBox.valueChanged.connect(self._set_plot_x_axis)
        +        self.comboBox = QComboBox()
        +        self.comboBox.addItems(['PeakCenter', 'PeakMax', 'PeakAverage'])
        +        self.comboBox.setMaximumWidth(150)
        +        
        +        self.label_28 = QLabel("Mass:")
        +        self.spinBox_7 = QSpinBox()
        +        self.spinBox_7.setMinimum(1)
        +        self.spinBox_7.setMaximum(200)
        +        self.spinBox_7.setValue(4)
        +        self.spinBox_7.setMaximumWidth(150)
        +        self.label_28.hide()
        +        self.spinBox_7.hide()
        +        self.label_32 = QLabel("Filter mode:")
        +        self.formLayout_3.addRow(self.label_29, self.spinBox)
        +        self.formLayout_3.addRow(self.label_30, self.spinBox_2)
        +        self.formLayout_3.addRow(self.label_28, self.spinBox_7)
        +        self.formLayout_3.addRow(self.label_32, self.comboBox)
        +        self.horizontalLayout.addLayout(self.formLayout_3)
        +        
        +        self.formLayout_4 = QFormLayout()
        +        self.label_33 = QLabel("Accuracy:")
        +        self.spinBox_3 = QSpinBox()
        +        self.spinBox_3.setMinimum(0)
        +        self.spinBox_3.setMaximum(10)
        +        self.spinBox_3.setValue(5)
        +        self.spinBox_3.setMinimumWidth(150)
        +        self.label_34 = QLabel("EGain index:")
        +        self.spinBox_4 = QSpinBox()
        +        self.spinBox_4.setMinimumWidth(150)
        +        self.label_35 = QLabel("Source index:")
        +        self.spinBox_5 = QSpinBox()
        +        self.spinBox_5.setMinimumWidth(150)
        +        self.label_36 = QLabel("Detector index:")
        +        self.spinBox_6 = QSpinBox()
        +        self.spinBox_6.setMinimumWidth(150)
        +        self.formLayout_4.addRow(self.label_33, self.spinBox_3)
        +        self.formLayout_4.addRow(self.label_34, self.spinBox_4)
        +        self.formLayout_4.addRow(self.label_35, self.spinBox_5)
        +        self.formLayout_4.addRow(self.label_36, self.spinBox_6)
        +        self.horizontalLayout.addLayout(self.formLayout_4)
        +        
        +        spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        +        self.horizontalLayout.addItem(spacerItem)
        +        
        +        self.formLayout_5 = QFormLayout()
        +        # self.comboBox_2 = QComboBox()
        +        # self.comboBox_2.addItems(['bar', 'peak', 'leak'])
        +        # self.comboBox_2.currentTextChanged.connect(self._on_mode_changed)
        +        # self.formLayout_5.addRow(QLabel("Measurement mode:"), self.comboBox_2)
        +        self.checkbox = QCheckBox("Continous measurement")
        +        self.formLayout_5.addRow(None, self.checkbox)
        +        
        +        
        +        self.pushButton = QPushButton("Turn off filament")
        +        self.pushButton.clicked.connect(self._on_toggle_filament)
        +        self.formLayout_5.addRow(QLabel(""), self.pushButton)
        +        self.pushButton_2 = QPushButton("Start scanning")
        +        self.pushButton_2.clicked.connect(self._on_toggle_scan)
        +        self.spinBox_8 = QSpinBox()
        +        self.spinBox_8.setMinimum(0)
        +        self.spinBox_8.setMaximum(1000)
        +        self.spinBox_8.setValue(10)
        +        self.formLayout_5.addRow(self.spinBox_8, self.pushButton_2)
        +        self.pushButton_3 = QPushButton("Restart scan")
        +        self.pushButton_3.clicked.connect(self._on_restart_scan)
        +        self.formLayout_5.addRow(QLabel(""), self.pushButton_3)
        +        self.horizontalLayout.addLayout(self.formLayout_5)
        +        
        +        self.groupBox_4.setLayout(self.horizontalLayout)
        +        self.groupBox_4.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum)
        +        self.gridLayout.addWidget(self.groupBox_4, 3, 0, 1, 3)
        +        
        +        # Plot widget
        +        self.canvas = mPlotCanvas()
        +        self.canvas.ax.set_xticks(range(self.spinBox.value(), self.spinBox_2.value(), 10))
        +        self.canvas.ax.grid(True, which='major')
        +        self.canvas.ax.grid(True, which='minor', linestyle='--')
        +        self.gridLayout.addWidget(self.canvas, 0, 1, 3, 2)
        +        self.canvas.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
        +        
        +        self.setCentralWidget(self.centerWidget)
        +    
        +    def _set_plot_x_axis(self):
        +        self.canvas.ax.set_xticks(range(self.spinBox.value(), self.spinBox_2.value(), 10))
        +    
        +    def _on_mode_changed(self):
        +        sender = self.sender()
        +        mode = sender.currentText()
        +        if mode == 'leak':
        +            self.label_28.show()
        +            self.label_29.hide()
        +            self.label_30.hide()
        +            self.spinBox.hide()
        +            self.spinBox_2.hide()
        +            self.spinBox_7.show()
        +        if mode == 'bar':
        +            self.label_28.hide()
        +            self.label_28.show()
        +            self.label_30.show()
        +            self.spinBox.show()
        +            self.spinBox_2.show()
        +            self.spinBox_7.hide()
        +        if mode == 'peak':
        +            self.label_28.hide()
        +            self.label_28.hide()
        +            self.label_30.hide()
        +            self.spinBox.hide()
        +            self.spinBox_2.hide()
        +            self.spinBox_7.hide()
        +    
        +    def _on_restart_scan(self):
        +        try:
        +            self.actionObservers({'restart_scan' : []})
        +        except Exception as ex:
        +            logger.exception(ex)
        +    
        +    def setEnableControls(self, state):
        +        self.label_29.setEnabled(state)
        +        self.label_30.setEnabled(state)
        +        self.label_28.setEnabled(state)
        +        self.label_32.setEnabled(state)
        +        self.spinBox.setEnabled(state)
        +        self.spinBox_7.setEnabled(state)
        +        self.spinBox_2.setEnabled(state)
        +        self.comboBox.setEnabled(state)
        +        
        +        self.label_33.setEnabled(state)
        +        self.label_34.setEnabled(state)
        +        self.label_35.setEnabled(state)
        +        self.label_36.setEnabled(state)
        +        self.spinBox_3.setEnabled(state)
        +        self.spinBox_4.setEnabled(state)
        +        self.spinBox_5.setEnabled(state)
        +        self.spinBox_6.setEnabled(state)
        +    
        +    def _on_toggle_scan(self):
        +        # sender = self.sender()
        +        
        +        # scan_type = self.comboBox_2.currentText()
        +        name = datetime.now().strftime("%m%d%Y%H%M%S")
        +        # if scan_type == 'bar':
        +        start_mass = self.spinBox.value()
        +        end_mass = self.spinBox_2.value()
        +        filter_mode = self.comboBox.currentText()
        +        accuracy = self.spinBox_3.value()
        +        e_gain_index = self.spinBox_4.value()
        +        source_index = self.spinBox_5.value()
        +        detector_index = self.spinBox_6.value()
        +        num_scans = self.spinBox_8.value()
        +        try:
        +            if 'Start' in self.pushButton_2.text():
        +                self.pushButton_2.setText("Stop scan")
        +                self.actionObservers({'toggle_scan' : [False]})
        +                self.actionObservers({'add_bar_chart' : [name,
        +                                                        start_mass,
        +                                                        end_mass,
        +                                                        filter_mode,
        +                                                        accuracy,
        +                                                        e_gain_index,
        +                                                        source_index,
        +                                                        detector_index]})
        +                
        +                logger.info(f'Starting scan {name}, Start mass: {start_mass}, End mass: {end_mass},')
        +                self.actionObservers({'toggle_scan' : [True, num_scans]})
        +            elif 'Stop' in self.pushButton_2.text():
        +                self.pushButton_2.setText("Start scan")
        +                logger.info(f'Stopping scan {name}, Start mass: {start_mass}, End mass: {end_mass},')
        +                self.actionObservers({'toggle_scan' : [False]})
        +        except Exception as ex:
        +            logger.exception(ex)
        +                
        +    def _on_toggle_filament(self):
        +        sender = self.sender()
        +        try:
        +            if 'off' in sender.text():
        +                logger.info("Turning on filament")
        +                self.actionObservers({'toggle_filament' : [False]})
        +            elif 'on' in sender.text():
        +                logger.info("Turning off filament")
        +                self.actionObservers({'toggle_filament' : [True]})
        +        except Exception as ex:
        +            logger.exception(ex)
        +    
        +    def _on_new_running_signal(self, monitoring_info):
        +        if monitoring_info:
        +            self.label_27.setText('True')
        +            self.pushButton_2.setText("Stop scan")
        +            self.pushButton_3.setEnabled(True)
        +            self.setEnableControls(False)
        +        else:
        +            if 'Stop' in self.pushButton_2.text():
        +                if self.checkbox.isChecked():
        +                    nScans = self.spinBox_8.value()
        +                    self.actionObservers({'resume_scan' : [nScans]})
        +            else:
        +                self.label_27.setText('False')
        +                self.pushButton_2.setText("Start scan")
        +                self.pushButton_3.setEnabled(False)
        +                self.setEnableControls(True)
        +                self.barRingBuffer = [np.nan] * 200
        +    
        +    def _on_new_scan_status_signal(self, monitoring_info):
        +        
        +        if 'Running' in monitoring_info:
        +            running = monitoring_info['Running']
        +            
        +            if running:
        +                self.pushButton_2.setText("Stop scan")
        +                self.pushButton_3.setEnabled(True)
        +            else:
        +                self.pushButton_2.setText("Start scan")
        +                self.pushButton_3.setEnabled(False)
        +                try:
        +                    self.actionObservers({'toggle_scan' : [False]})
        +                except Exception as ex:
        +                    logger.exectpion(ex)
        +            
        +
        +        self.label_20.setText(monitoring_info['StartMass'])
        +        self.label_21.setText(monitoring_info['EndMass'])
        +        self.label_22.setText(monitoring_info['FilterMode'])
        +        self.label_23.setText(monitoring_info['Accuracy'])
        +        self.label_24.setText(monitoring_info['EGainIndex'])
        +        self.label_25.setText(monitoring_info['SourceIndex'])
        +        self.label_26.setText(monitoring_info['DetectorIndex'])
        +        self.label_27.setText(str(monitoring_info['Running']))
        +    
        +    def _on_new_data_signal(self, monitoring_info):
        +        if monitoring_info != []:
        +            for i, value in enumerate(monitoring_info):
        +                if value != 0:
        +                    self.barRingBuffer[i] = value
        +            
        +            x = range(1, len(self.barRingBuffer)+1)
        +            self.canvas.ax.clear()
        +            self.canvas.ax.set_ylabel("Pressure")
        +            self.canvas.ax.set_yscale("log")
        +            self.canvas.ax.grid(True, which='major')
        +            self.canvas.ax.grid(True, which='minor', linestyle='--')
        +            self.canvas.ax.set_ylim((1E-9, 1E-4))
        +            self.canvas.ax.set_xlim((1, self.spinBox_2.value()))
        +            self.canvas.ax.bar(x, self.barRingBuffer, align='center')
        +            self.canvas.draw()
        +
        +    def _on_new_info_signal(self, monitoring_info):
        +        self.label_7.setText(monitoring_info['Name'])
        +        self.label_8.setText(monitoring_info['SerialNumber'])
        +        self.label_9.setText(monitoring_info['UserApplication'])
        +        self.label_10.setText(monitoring_info['UserAddress'])
        +        self.label_11.setText(monitoring_info['State'])
        +        self.label_12.setText(monitoring_info['ProductID'])
        +        self.label_13.setText(monitoring_info['DetectorType'])
        +        self.label_14.setText(monitoring_info['TotalPressureGauge'])
        +        self.label_15.setText(monitoring_info['FilamentType'])
        +        self.label_16.setText(monitoring_info['SensorType'])
        +        self.label_17.setText(monitoring_info['MaxMass'])
        +    
        +    def _on_new_filament_signal(self, monitoring_info):
        +        self.label_2.setText(monitoring_info['SummaryState'])
        +        
        +        if monitoring_info['SummaryState'] == 'OFF':
        +            self.pushButton.setText('Turn on filament')
        +        else:
        +            self.pushButton.setText('Turn off filament')
        +
        +        self.label_3.setText(monitoring_info['ActiveFilament'])
        +        self.label_4.setText(monitoring_info['MaxOnTime'])
        +        self.label_5.setText(monitoring_info['OnTimeRemaining'])
        +    
        +
        +class RGAUIModel:
        +    def __init__(self, mode):
        +        self.mode = mode
        +        
        +        if self.mode == 'proxy':
        +            try:
        +                self.rga = EvisionProxy()
        +            except Exception as exc:
        +                raise exc
        +            
        +        elif self.mode == 'simulator':
        +            self.rga = EvisionSimulator()
        +        else:
        +            raise ValueError(f'Unknown type of rga implementation passed into the model')
        +            
        +            if self.rga is not None:
        +                logger.debug(f'RGA Controller initialized as {self.rga.__class__.__name__}')
        +
        +    
        +    def stop_scan(self):
        +        self.rga.stop_scan()
        +        self.rga.measurement_remove_all()
        +    
        +    def resume_scan(self):
        +        pass
        +       
        +    def restart_scan(self):
        +        self.rga.restart_scan()
        +        
        +    def resume_scan(self, num):
        +        self.rga.resume_scan(num)
        +    
        +    def start_scan(self, num):
        +        self.rga.start_scan(num)
        +    
        +    def filament_control(self, state):
        +        self.rga.filament_control(state)
        +
        +    def add_bar_chart(self, name, startMass, endMass, filterMode, accuracy, eGainIndex, sourceIndex, detectorIndex):
        +        self.rga.add_bar_chart(name, startMass, endMass, filterMode, accuracy, eGainIndex, sourceIndex, detectorIndex)
        +
        +    def add_peak_jump(self, name, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +        self.rga.add_peak_jump(name, filterMode, accuracy, eGainIndex, sourceIndex, detectorIndex)
        +
        +    def add_single_peak(self, name, mass, accuracy, eGainIndex, sourceIndex, detectorIndex):
        +        self.rga.add_single_peak(name, mass, accuracy, eGainIndex, sourceIndex, detectorIndex)
        +    
        +    def add_scan(self, name):
        +        self.rga.add_scan(name)
        +
        +class RGAUIController(Observer):
        +    def __init__(self, model: RGAUIModel, view:RGAUIView):
        +        self.model = model
        +        self.view  = view
        +        self.view.addObserver(self)
        +        
        +    def update(self, changed_object):
        +        text = changed_object.text()
        +        
        +        if text == "Reconnect":
        +            if changed_object.isChecked():
        +                logger.debug("Reconnecting the RGA model.")
        +
        +            if self.model.reconnect():
        +                self.view.set_connection_state(True)
        +                
        +                if not self.model.has_commands():
        +                    self.model.load_commands()  
        +            else:
        +                self.view.reconnect_action.setChecked(False)
        +        else:
        +            self.view.reconnect_action.setChecked(False)
        +            self.model.disconnect()
        +            self.view.set_connection_state(False)
        +            
        +        return
        +    
        +    def do(self, actions):
        +        for action, value in actions.items():
        +            if action == "toggle_filament":
        +                self.model.filament_control(value[0])
        +            if action == 'toggle_scan':   
        +                if value[0]:
        +                    self.model.start_scan(value[1])
        +                else:
        +                    self.model.stop_scan()
        +            if action == 'add_bar_chart':
        +                self.model.stop_scan()
        +                self.model.add_bar_chart(value[0], value[1], value[2],value[3],value[4],value[5], value[6], value[7])
        +                self.model.add_scan(value[0])
        +            if action == 'restart_scan':
        +                self.model.restart_scan()
        +            if action == 'resume_scan':
        +                self.model.resume_scan(value[0])
        +        
        +def parse_arguments():
        +    """
        +    Prepare the arguments that are specific for this application.
        +    """
        +    parser = argparse.ArgumentParser()
        +    parser.add_argument(
        +        "--type",
        +        dest="type",
        +        action="store",
        +        choices={"proxy", "simulator", "crio"},
        +        help="Specify AEU cRIO implementation you want to connect to.",
        +        default="proxy",
        +    )    
        +    args = parser.parse_args()
        +    return args
        +
        +def main():
        +    args = list(sys.argv)
        +
        +    app = QApplication(args)
        +    
        +    args = parse_arguments()
        +
        +    view = RGAUIView()
        +    model = RGAUIModel(args.type)
        +    controller = RGAUIController(model, view)
        +    
        +    view.show()
        +    sys.exit(app.exec_())
        +
        +if __name__ == "__main__":
        +    main()
        +
        +
        +
        +
        +
        +
        +
        +

        Functions

        +
        +
        +def main() +
        +
        +
        +
        + +Expand source code + +
        def main():
        +    args = list(sys.argv)
        +
        +    app = QApplication(args)
        +    
        +    args = parse_arguments()
        +
        +    view = RGAUIView()
        +    model = RGAUIModel(args.type)
        +    controller = RGAUIController(model, view)
        +    
        +    view.show()
        +    sys.exit(app.exec_())
        +
        +
        +
        +def parse_arguments() +
        +
        +

        Prepare the arguments that are specific for this application.

        +
        + +Expand source code + +
        def parse_arguments():
        +    """
        +    Prepare the arguments that are specific for this application.
        +    """
        +    parser = argparse.ArgumentParser()
        +    parser.add_argument(
        +        "--type",
        +        dest="type",
        +        action="store",
        +        choices={"proxy", "simulator", "crio"},
        +        help="Specify AEU cRIO implementation you want to connect to.",
        +        default="proxy",
        +    )    
        +    args = parser.parse_args()
        +    return args
        +
        +
        +
        +
        +
        +

        Classes

        +
        +
        +class RGAMonitoringWorker +
        +
        +

        QObject(parent: typing.Optional[QObject] = None)

        +

        Initialisation of a monitoring worker.

        +

        This worker keeps an eye on the monitoring port of the Beaglebone rga. When a change in +Relevant information occurs, a signalw ill be emitted. These signals will be used to update the GUI

        +
        + +Expand source code + +
        class RGAMonitoringWorker(QObject):
        +
        +    rga_new_data_signal         = pyqtSignal(list)
        +    rga_new_info_signal         = pyqtSignal(dict)
        +    rga_new_filament_signal     = pyqtSignal(dict)
        +    rga_new_scan_status_signal  = pyqtSignal(dict)
        +    rga_new_running_signal      = pyqtSignal(bool)
        +    
        +    def __init__(self):
        +        """ Initialisation of a monitoring worker.
        +        
        +        This worker keeps an eye on the monitoring port of the Beaglebone rga. When a change in
        +            Relevant information occurs, a signalw ill be emitted. These signals will be used to update the GUI
        +        """
        +        
        +        
        +        super(RGAMonitoringWorker, self).__init__()
        +        
        +        self.active = False
        +        self.just_reconnected = True
        +        
        +        self.monitoring_socket = None
        +        self.is_socket_connected = True
        +        self.monitoring_timeout = 0.5
        +        
        +        self.connect_socket()
        +        
        +        # Keep track of the rga status, so we only have to send a signal when the state has changed
        +        
        +        self.previous_rga_data      = {}
        +        self.previous_rga_info      = {}
        +        self.previous_filament_info = {}
        +        self.previous_scan_reading  = {}
        +        self.previous_running       = None
        +        
        +    def connect_socket(self):
        +        """ Create a socket and connect to the monitoring port.
        +        """
        +        
        +
        +        try:
        +            transport   = CTRL_SETTINGS.PROTOCOL
        +            hostname    = CTRL_SETTINGS.HOSTNAME
        +            
        +            monitoring_port = CTRL_SETTINGS.MONITORING_PORT
        +            monitoring_address = connect_address(transport, hostname, monitoring_port)
        +            
        +            self.monitoring_socket = zmq.Context().socket(zmq.SUB)
        +            self.monitoring_socket.connect(monitoring_address)
        +            self.monitoring_socket.setsockopt_string(zmq.SUBSCRIBE, "")
        +            
        +            self.monitoring_timeout = 0.5
        +            
        +            self.is_socket_connected = True
        +            
        +        except:
        +            self.is_socket_connected = False
        +            
        +    def stop(self):
        +        
        +        """ Stop the monitoring worker.
        +
        +        The monitoring socket is disconnected from the monitoring port and is then closed immediately.
        +        """
        +        
        +        self.monitoring_socket.close()
        +        self.is_socket_connected = False
        +        
        +        self.active = False
        +        
        +    def start_process(self):
        +        """Start updated the Beaglebone status"""
        +        self.run()
        +        
        +    @pyqtSlot()
        +    def run(self):
        +        """Keep on checkin whether the Beaglebone state has changed
        +        
        +        If the beaglebone status has changed, update it in the GUI
        +
        +        Raises:
        +            Exception: ZMQ Error
        +        """
        +        
        +        self.active = True
        +        while self.is_socket_connected and self.active:
        +            
        +            try:
        +                socket_list, _, exc_list = zmq.select([self.monitoring_socket], [], [], timeout=self.monitoring_timeout)
        +
        +                if self.monitoring_socket in socket_list:
        +                    try:
        +                        pickle_string = self.monitoring_socket.recv()
        +                    except Exception:
        +                        raise Exception
        +
        +                    monitoring_info = pickle.loads(pickle_string)
        +                    
        +                    # logger.info(monitoring_info)
        +                
        +                    if 'MassReading' in monitoring_info:
        +                        mass_reading = monitoring_info['MassReading']
        +                        if mass_reading != self.previous_rga_data:
        +                            self.rga_new_data_signal.emit(mass_reading)
        +                        else:
        +                            running = False
        +                            if running != self.previous_running:
        +                                self.rga_new_running_signal.emit(running)
        +                        self.previous_rga_data = mass_reading
        +                    
        +                    if 'FilamentStatus' in monitoring_info:
        +                        info_reading = monitoring_info['FilamentStatus']
        +                        if info_reading != self.previous_filament_info:
        +                            self.rga_new_filament_signal.emit(info_reading)
        +                        self.previous_filament_info = info_reading
        +                        
        +                    if 'RGAStatus' in monitoring_info:
        +                        status_reading = monitoring_info['RGAStatus']
        +                        if status_reading != self.previous_rga_info:
        +                            self.rga_new_info_signal.emit(status_reading)
        +                        self.previous_filament_info = status_reading
        +                
        +                    if 'ScanStatus' in monitoring_info:
        +                        scan_reading = monitoring_info['ScanStatus']
        +                        if scan_reading != self.previous_scan_reading:
        +                            self.rga_new_scan_status_signal.emit(scan_reading)
        +                        self.previous_scan_reading = scan_reading
        +                
        +            except zmq.ZMQError as exc:
        +                raise exc
        +
        +

        Ancestors

        +
          +
        • PyQt5.QtCore.QObject
        • +
        • sip.wrapper
        • +
        • sip.simplewrapper
        • +
        +

        Methods

        +
        +
        +def connect_socket(self) +
        +
        +

        Create a socket and connect to the monitoring port.

        +
        + +Expand source code + +
        def connect_socket(self):
        +    """ Create a socket and connect to the monitoring port.
        +    """
        +    
        +
        +    try:
        +        transport   = CTRL_SETTINGS.PROTOCOL
        +        hostname    = CTRL_SETTINGS.HOSTNAME
        +        
        +        monitoring_port = CTRL_SETTINGS.MONITORING_PORT
        +        monitoring_address = connect_address(transport, hostname, monitoring_port)
        +        
        +        self.monitoring_socket = zmq.Context().socket(zmq.SUB)
        +        self.monitoring_socket.connect(monitoring_address)
        +        self.monitoring_socket.setsockopt_string(zmq.SUBSCRIBE, "")
        +        
        +        self.monitoring_timeout = 0.5
        +        
        +        self.is_socket_connected = True
        +        
        +    except:
        +        self.is_socket_connected = False
        +
        +
        +
        +def rga_new_data_signal(...) +
        +
        +
        +
        +
        +def rga_new_filament_signal(...) +
        +
        +
        +
        +
        +def rga_new_info_signal(...) +
        +
        +
        +
        +
        +def rga_new_running_signal(...) +
        +
        +
        +
        +
        +def rga_new_scan_status_signal(...) +
        +
        +
        +
        +
        +def run(self) +
        +
        +

        Keep on checkin whether the Beaglebone state has changed

        +

        If the beaglebone status has changed, update it in the GUI

        +

        Raises

        +
        +
        Exception
        +
        ZMQ Error
        +
        +
        + +Expand source code + +
        @pyqtSlot()
        +def run(self):
        +    """Keep on checkin whether the Beaglebone state has changed
        +    
        +    If the beaglebone status has changed, update it in the GUI
        +
        +    Raises:
        +        Exception: ZMQ Error
        +    """
        +    
        +    self.active = True
        +    while self.is_socket_connected and self.active:
        +        
        +        try:
        +            socket_list, _, exc_list = zmq.select([self.monitoring_socket], [], [], timeout=self.monitoring_timeout)
        +
        +            if self.monitoring_socket in socket_list:
        +                try:
        +                    pickle_string = self.monitoring_socket.recv()
        +                except Exception:
        +                    raise Exception
        +
        +                monitoring_info = pickle.loads(pickle_string)
        +                
        +                # logger.info(monitoring_info)
        +            
        +                if 'MassReading' in monitoring_info:
        +                    mass_reading = monitoring_info['MassReading']
        +                    if mass_reading != self.previous_rga_data:
        +                        self.rga_new_data_signal.emit(mass_reading)
        +                    else:
        +                        running = False
        +                        if running != self.previous_running:
        +                            self.rga_new_running_signal.emit(running)
        +                    self.previous_rga_data = mass_reading
        +                
        +                if 'FilamentStatus' in monitoring_info:
        +                    info_reading = monitoring_info['FilamentStatus']
        +                    if info_reading != self.previous_filament_info:
        +                        self.rga_new_filament_signal.emit(info_reading)
        +                    self.previous_filament_info = info_reading
        +                    
        +                if 'RGAStatus' in monitoring_info:
        +                    status_reading = monitoring_info['RGAStatus']
        +                    if status_reading != self.previous_rga_info:
        +                        self.rga_new_info_signal.emit(status_reading)
        +                    self.previous_filament_info = status_reading
        +            
        +                if 'ScanStatus' in monitoring_info:
        +                    scan_reading = monitoring_info['ScanStatus']
        +                    if scan_reading != self.previous_scan_reading:
        +                        self.rga_new_scan_status_signal.emit(scan_reading)
        +                    self.previous_scan_reading = scan_reading
        +            
        +        except zmq.ZMQError as exc:
        +            raise exc
        +
        +
        +
        +def start_process(self) +
        +
        +

        Start updated the Beaglebone status

        +
        + +Expand source code + +
        def start_process(self):
        +    """Start updated the Beaglebone status"""
        +    self.run()
        +
        +
        +
        +def stop(self) +
        +
        +

        Stop the monitoring worker.

        +

        The monitoring socket is disconnected from the monitoring port and is then closed immediately.

        +
        + +Expand source code + +
        def stop(self):
        +    
        +    """ Stop the monitoring worker.
        +
        +    The monitoring socket is disconnected from the monitoring port and is then closed immediately.
        +    """
        +    
        +    self.monitoring_socket.close()
        +    self.is_socket_connected = False
        +    
        +    self.active = False
        +
        +
        +
        +
        +
        +class RGAUIController +(model: RGAUIModel, view: RGAUIView) +
        +
        +

        Helper class that provides a standard way to create an ABC using +inheritance.

        +
        + +Expand source code + +
        class RGAUIController(Observer):
        +    def __init__(self, model: RGAUIModel, view:RGAUIView):
        +        self.model = model
        +        self.view  = view
        +        self.view.addObserver(self)
        +        
        +    def update(self, changed_object):
        +        text = changed_object.text()
        +        
        +        if text == "Reconnect":
        +            if changed_object.isChecked():
        +                logger.debug("Reconnecting the RGA model.")
        +
        +            if self.model.reconnect():
        +                self.view.set_connection_state(True)
        +                
        +                if not self.model.has_commands():
        +                    self.model.load_commands()  
        +            else:
        +                self.view.reconnect_action.setChecked(False)
        +        else:
        +            self.view.reconnect_action.setChecked(False)
        +            self.model.disconnect()
        +            self.view.set_connection_state(False)
        +            
        +        return
        +    
        +    def do(self, actions):
        +        for action, value in actions.items():
        +            if action == "toggle_filament":
        +                self.model.filament_control(value[0])
        +            if action == 'toggle_scan':   
        +                if value[0]:
        +                    self.model.start_scan(value[1])
        +                else:
        +                    self.model.stop_scan()
        +            if action == 'add_bar_chart':
        +                self.model.stop_scan()
        +                self.model.add_bar_chart(value[0], value[1], value[2],value[3],value[4],value[5], value[6], value[7])
        +                self.model.add_scan(value[0])
        +            if action == 'restart_scan':
        +                self.model.restart_scan()
        +            if action == 'resume_scan':
        +                self.model.resume_scan(value[0])
        +
        +

        Ancestors

        + +

        Methods

        +
        +
        +def do(self, actions) +
        +
        +
        +
        + +Expand source code + +
        def do(self, actions):
        +    for action, value in actions.items():
        +        if action == "toggle_filament":
        +            self.model.filament_control(value[0])
        +        if action == 'toggle_scan':   
        +            if value[0]:
        +                self.model.start_scan(value[1])
        +            else:
        +                self.model.stop_scan()
        +        if action == 'add_bar_chart':
        +            self.model.stop_scan()
        +            self.model.add_bar_chart(value[0], value[1], value[2],value[3],value[4],value[5], value[6], value[7])
        +            self.model.add_scan(value[0])
        +        if action == 'restart_scan':
        +            self.model.restart_scan()
        +        if action == 'resume_scan':
        +            self.model.resume_scan(value[0])
        +
        +
        +
        +def update(self, changed_object) +
        +
        +
        +
        + +Expand source code + +
        def update(self, changed_object):
        +    text = changed_object.text()
        +    
        +    if text == "Reconnect":
        +        if changed_object.isChecked():
        +            logger.debug("Reconnecting the RGA model.")
        +
        +        if self.model.reconnect():
        +            self.view.set_connection_state(True)
        +            
        +            if not self.model.has_commands():
        +                self.model.load_commands()  
        +        else:
        +            self.view.reconnect_action.setChecked(False)
        +    else:
        +        self.view.reconnect_action.setChecked(False)
        +        self.model.disconnect()
        +        self.view.set_connection_state(False)
        +        
        +    return
        +
        +
        +
        +
        +
        +class RGAUIModel +(mode) +
        +
        +
        +
        + +Expand source code + +
        class RGAUIModel:
        +    def __init__(self, mode):
        +        self.mode = mode
        +        
        +        if self.mode == 'proxy':
        +            try:
        +                self.rga = EvisionProxy()
        +            except Exception as exc:
        +                raise exc
        +            
        +        elif self.mode == 'simulator':
        +            self.rga = EvisionSimulator()
        +        else:
        +            raise ValueError(f'Unknown type of rga implementation passed into the model')
        +            
        +            if self.rga is not None:
        +                logger.debug(f'RGA Controller initialized as {self.rga.__class__.__name__}')
        +
        +    
        +    def stop_scan(self):
        +        self.rga.stop_scan()
        +        self.rga.measurement_remove_all()
        +    
        +    def resume_scan(self):
        +        pass
        +       
        +    def restart_scan(self):
        +        self.rga.restart_scan()
        +        
        +    def resume_scan(self, num):
        +        self.rga.resume_scan(num)
        +    
        +    def start_scan(self, num):
        +        self.rga.start_scan(num)
        +    
        +    def filament_control(self, state):
        +        self.rga.filament_control(state)
        +
        +    def add_bar_chart(self, name, startMass, endMass, filterMode, accuracy, eGainIndex, sourceIndex, detectorIndex):
        +        self.rga.add_bar_chart(name, startMass, endMass, filterMode, accuracy, eGainIndex, sourceIndex, detectorIndex)
        +
        +    def add_peak_jump(self, name, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +        self.rga.add_peak_jump(name, filterMode, accuracy, eGainIndex, sourceIndex, detectorIndex)
        +
        +    def add_single_peak(self, name, mass, accuracy, eGainIndex, sourceIndex, detectorIndex):
        +        self.rga.add_single_peak(name, mass, accuracy, eGainIndex, sourceIndex, detectorIndex)
        +    
        +    def add_scan(self, name):
        +        self.rga.add_scan(name)
        +
        +

        Methods

        +
        +
        +def add_bar_chart(self, name, startMass, endMass, filterMode, accuracy, eGainIndex, sourceIndex, detectorIndex) +
        +
        +
        +
        + +Expand source code + +
        def add_bar_chart(self, name, startMass, endMass, filterMode, accuracy, eGainIndex, sourceIndex, detectorIndex):
        +    self.rga.add_bar_chart(name, startMass, endMass, filterMode, accuracy, eGainIndex, sourceIndex, detectorIndex)
        +
        +
        +
        +def add_peak_jump(self, name, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0) +
        +
        +
        +
        + +Expand source code + +
        def add_peak_jump(self, name, filterMode='PeakCenter', accuracy=5, eGainIndex=0, sourceIndex=0, detectorIndex=0):
        +    self.rga.add_peak_jump(name, filterMode, accuracy, eGainIndex, sourceIndex, detectorIndex)
        +
        +
        +
        +def add_scan(self, name) +
        +
        +
        +
        + +Expand source code + +
        def add_scan(self, name):
        +    self.rga.add_scan(name)
        +
        +
        +
        +def add_single_peak(self, name, mass, accuracy, eGainIndex, sourceIndex, detectorIndex) +
        +
        +
        +
        + +Expand source code + +
        def add_single_peak(self, name, mass, accuracy, eGainIndex, sourceIndex, detectorIndex):
        +    self.rga.add_single_peak(name, mass, accuracy, eGainIndex, sourceIndex, detectorIndex)
        +
        +
        +
        +def filament_control(self, state) +
        +
        +
        +
        + +Expand source code + +
        def filament_control(self, state):
        +    self.rga.filament_control(state)
        +
        +
        +
        +def restart_scan(self) +
        +
        +
        +
        + +Expand source code + +
        def restart_scan(self):
        +    self.rga.restart_scan()
        +
        +
        +
        +def resume_scan(self, num) +
        +
        +
        +
        + +Expand source code + +
        def resume_scan(self, num):
        +    self.rga.resume_scan(num)
        +
        +
        +
        +def start_scan(self, num) +
        +
        +
        +
        + +Expand source code + +
        def start_scan(self, num):
        +    self.rga.start_scan(num)
        +
        +
        +
        +def stop_scan(self) +
        +
        +
        +
        + +Expand source code + +
        def stop_scan(self):
        +    self.rga.stop_scan()
        +    self.rga.measurement_remove_all()
        +
        +
        +
        +
        +
        +class RGAUIView +(*args, **kwargs) +
        +
        +

        QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

        +
        + +Expand source code + +
        class RGAUIView(QMainWindow, Observable):
        +    def __init__(self, *args, **kwargs):
        +        super(RGAUIView, self).__init__(*args, **kwargs)
        +        
        +        self.setWindowTitle("E-Vision Residual Gas Analyzer")
        +        self.setGeometry(300, 300, 1000, 700)
        +        self.initUI()
        +        
        +        self.rga_monitoring_thread = QThread()
        +        self.rga_monitoring_worker = RGAMonitoringWorker()
        +        self.rga_monitoring_worker.moveToThread(self.rga_monitoring_thread)
        +        
        +        self.rga_monitoring_worker.rga_new_data_signal.connect(self._on_new_data_signal)
        +        self.rga_monitoring_worker.rga_new_info_signal.connect(self._on_new_info_signal)
        +        self.rga_monitoring_worker.rga_new_filament_signal.connect(self._on_new_filament_signal)
        +        self.rga_monitoring_worker.rga_new_scan_status_signal.connect(self._on_new_scan_status_signal)
        +        self.rga_monitoring_worker.rga_new_running_signal.connect(self._on_new_running_signal)
        +        
        +        self.rga_monitoring_thread.started.connect(self.rga_monitoring_worker.start_process)
        +        
        +        self.rga_monitoring_thread.start()
        +        
        +        self.barRingBuffer = [np.nan] * 201
        +        
        +        self.single_line = [[],[]]
        +        self.peak_lines  = [{},[]]
        +        
        +    def initUI(self):
        +        self.centerWidget = QWidget(self)
        +        
        +        self.gridLayout = QGridLayout(self.centerWidget)
        +        
        +        # Filament groupbox
        +        self.groupBox   = QGroupBox()
        +        self.verticalLayout = QVBoxLayout()
        +        self.label = QLabel("<b>Filament information</b>")
        +        self.formLayout = QFormLayout()
        +        self.label_2 = QLabel("")
        +        self.label_3 = QLabel("")
        +        self.label_4 = QLabel("")
        +        self.label_5 = QLabel("")
        +        self.formLayout.addRow(QLabel("Summary state"), self.label_2)
        +        self.formLayout.addRow(QLabel("Active filament"), self.label_3)
        +        self.formLayout.addRow(QLabel("Max on time"), self.label_4)
        +        self.verticalLayout.addWidget(self.label)
        +        self.verticalLayout.addLayout(self.formLayout)
        +        self.groupBox.setLayout(self.verticalLayout)
        +        self.gridLayout.addWidget(self.groupBox, 0, 0, 1 ,1)
        +        self.groupBox.setMinimumWidth(300)
        +        self.groupBox.setMaximumWidth(400)
        +        self.groupBox.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        +        
        +        
        +        # Sensor groupbox
        +        self.groupBox_2 = QGroupBox()
        +        self.verticalLayout_2 = QVBoxLayout()
        +        self.label_6 = QLabel("<b>Sensor information</b>")
        +        self.formLayout_2 = QFormLayout()
        +        self.label_7 = QLabel("")
        +        self.label_8 = QLabel("")
        +        self.label_9 = QLabel("")
        +        self.label_10 = QLabel("")
        +        self.label_11 = QLabel("")
        +        self.label_12 = QLabel("")
        +        self.label_13 = QLabel("")
        +        self.label_14 = QLabel("")
        +        self.label_15 = QLabel("")
        +        self.label_16 = QLabel("")
        +        self.label_17 = QLabel("")
        +        self.formLayout_2.addRow(QLabel("Name"), self.label_7)
        +        self.formLayout_2.addRow(QLabel("Serial number"), self.label_8)
        +        self.formLayout_2.addRow(QLabel("User application"), self.label_9)
        +        self.formLayout_2.addRow(QLabel("User address"), self.label_10)
        +        self.formLayout_2.addRow(QLabel("State"), self.label_11)
        +        self.formLayout_2.addRow(QLabel("Product ID"), self.label_12)
        +        self.formLayout_2.addRow(QLabel("Detector type"), self.label_13)
        +        self.formLayout_2.addRow(QLabel("Total pressure gauge"), self.label_14)
        +        self.formLayout_2.addRow(QLabel("Filament type"), self.label_15)
        +        self.formLayout_2.addRow(QLabel("Sensor type"), self.label_16)
        +        self.formLayout_2.addRow(QLabel("Max mass"), self.label_17)
        +        self.verticalLayout_2.addWidget(self.label_6)
        +        self.verticalLayout_2.addLayout(self.formLayout_2)
        +        self.groupBox_2.setLayout(self.verticalLayout_2)
        +        self.gridLayout.addWidget(self.groupBox_2, 1, 0, 1 ,1)
        +        self.groupBox_2.setMinimumWidth(300)
        +        self.groupBox_2.setMaximumWidth(400)
        +        self.groupBox_2.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        +        
        +        # Scan status
        +        self.groupBox_3 = QGroupBox()
        +        self.verticalLayout_3 = QVBoxLayout()
        +        self.label_18 = QLabel("<b>Scan information</b>")
        +        self.formLayout_6 = QFormLayout()
        +        self.label_20 = QLabel("")
        +        self.label_21 = QLabel("")
        +        self.label_22 = QLabel("")
        +        self.label_23 = QLabel("")
        +        self.label_24 = QLabel("")
        +        self.label_25 = QLabel("")
        +        self.label_26 = QLabel("")
        +        self.label_27 = QLabel("")
        +        self.formLayout_6.addRow(QLabel("Start mass"), self.label_20)
        +        self.formLayout_6.addRow(QLabel("End mass"), self.label_21)
        +        self.formLayout_6.addRow(QLabel("Filter mode"), self.label_22)
        +        self.formLayout_6.addRow(QLabel("Accuracy"), self.label_23)
        +        self.formLayout_6.addRow(QLabel("E Gain Index"), self.label_24)
        +        self.formLayout_6.addRow(QLabel("Source index"), self.label_25)
        +        self.formLayout_6.addRow(QLabel("Detector index"), self.label_26)
        +        self.formLayout_6.addRow(QLabel("Running"), self.label_27)
        +        self.verticalLayout_3.addWidget(self.label_18)
        +        self.verticalLayout_3.addLayout(self.formLayout_6)
        +        self.groupBox_3.setLayout(self.verticalLayout_3)
        +        self.gridLayout.addWidget(self.groupBox_3, 2, 0 ,1, 1)
        +        self.groupBox_3.setMinimumWidth(300)
        +        self.groupBox_3.setMaximumWidth(400)
        +        self.groupBox_3.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        +        # spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        +
        +        
        +        # Plot navigation groupbox
        +        self.groupBox_4 = QGroupBox()
        +        self.horizontalLayout = QHBoxLayout()
        +        self.formLayout_3 = QFormLayout()
        +        self.label_29 = QLabel("Start mass:")
        +        self.spinBox = QSpinBox()
        +        self.spinBox.setMinimum(1)
        +        self.spinBox.setMaximum(200)
        +        self.spinBox.setValue(1)
        +        self.spinBox.setMaximumWidth(150)
        +        self.spinBox.valueChanged.connect(self._set_plot_x_axis)
        +        self.label_30 = QLabel("End mass:")
        +        self.spinBox_2 = QSpinBox()
        +        self.spinBox_2.setMinimum(2)
        +        self.spinBox_2.setMaximum(200)
        +        self.spinBox_2.setValue(100)
        +        self.spinBox_2.setMaximumWidth(150)
        +        self.spinBox.valueChanged.connect(self._set_plot_x_axis)
        +        self.comboBox = QComboBox()
        +        self.comboBox.addItems(['PeakCenter', 'PeakMax', 'PeakAverage'])
        +        self.comboBox.setMaximumWidth(150)
        +        
        +        self.label_28 = QLabel("Mass:")
        +        self.spinBox_7 = QSpinBox()
        +        self.spinBox_7.setMinimum(1)
        +        self.spinBox_7.setMaximum(200)
        +        self.spinBox_7.setValue(4)
        +        self.spinBox_7.setMaximumWidth(150)
        +        self.label_28.hide()
        +        self.spinBox_7.hide()
        +        self.label_32 = QLabel("Filter mode:")
        +        self.formLayout_3.addRow(self.label_29, self.spinBox)
        +        self.formLayout_3.addRow(self.label_30, self.spinBox_2)
        +        self.formLayout_3.addRow(self.label_28, self.spinBox_7)
        +        self.formLayout_3.addRow(self.label_32, self.comboBox)
        +        self.horizontalLayout.addLayout(self.formLayout_3)
        +        
        +        self.formLayout_4 = QFormLayout()
        +        self.label_33 = QLabel("Accuracy:")
        +        self.spinBox_3 = QSpinBox()
        +        self.spinBox_3.setMinimum(0)
        +        self.spinBox_3.setMaximum(10)
        +        self.spinBox_3.setValue(5)
        +        self.spinBox_3.setMinimumWidth(150)
        +        self.label_34 = QLabel("EGain index:")
        +        self.spinBox_4 = QSpinBox()
        +        self.spinBox_4.setMinimumWidth(150)
        +        self.label_35 = QLabel("Source index:")
        +        self.spinBox_5 = QSpinBox()
        +        self.spinBox_5.setMinimumWidth(150)
        +        self.label_36 = QLabel("Detector index:")
        +        self.spinBox_6 = QSpinBox()
        +        self.spinBox_6.setMinimumWidth(150)
        +        self.formLayout_4.addRow(self.label_33, self.spinBox_3)
        +        self.formLayout_4.addRow(self.label_34, self.spinBox_4)
        +        self.formLayout_4.addRow(self.label_35, self.spinBox_5)
        +        self.formLayout_4.addRow(self.label_36, self.spinBox_6)
        +        self.horizontalLayout.addLayout(self.formLayout_4)
        +        
        +        spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        +        self.horizontalLayout.addItem(spacerItem)
        +        
        +        self.formLayout_5 = QFormLayout()
        +        # self.comboBox_2 = QComboBox()
        +        # self.comboBox_2.addItems(['bar', 'peak', 'leak'])
        +        # self.comboBox_2.currentTextChanged.connect(self._on_mode_changed)
        +        # self.formLayout_5.addRow(QLabel("Measurement mode:"), self.comboBox_2)
        +        self.checkbox = QCheckBox("Continous measurement")
        +        self.formLayout_5.addRow(None, self.checkbox)
        +        
        +        
        +        self.pushButton = QPushButton("Turn off filament")
        +        self.pushButton.clicked.connect(self._on_toggle_filament)
        +        self.formLayout_5.addRow(QLabel(""), self.pushButton)
        +        self.pushButton_2 = QPushButton("Start scanning")
        +        self.pushButton_2.clicked.connect(self._on_toggle_scan)
        +        self.spinBox_8 = QSpinBox()
        +        self.spinBox_8.setMinimum(0)
        +        self.spinBox_8.setMaximum(1000)
        +        self.spinBox_8.setValue(10)
        +        self.formLayout_5.addRow(self.spinBox_8, self.pushButton_2)
        +        self.pushButton_3 = QPushButton("Restart scan")
        +        self.pushButton_3.clicked.connect(self._on_restart_scan)
        +        self.formLayout_5.addRow(QLabel(""), self.pushButton_3)
        +        self.horizontalLayout.addLayout(self.formLayout_5)
        +        
        +        self.groupBox_4.setLayout(self.horizontalLayout)
        +        self.groupBox_4.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum)
        +        self.gridLayout.addWidget(self.groupBox_4, 3, 0, 1, 3)
        +        
        +        # Plot widget
        +        self.canvas = mPlotCanvas()
        +        self.canvas.ax.set_xticks(range(self.spinBox.value(), self.spinBox_2.value(), 10))
        +        self.canvas.ax.grid(True, which='major')
        +        self.canvas.ax.grid(True, which='minor', linestyle='--')
        +        self.gridLayout.addWidget(self.canvas, 0, 1, 3, 2)
        +        self.canvas.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
        +        
        +        self.setCentralWidget(self.centerWidget)
        +    
        +    def _set_plot_x_axis(self):
        +        self.canvas.ax.set_xticks(range(self.spinBox.value(), self.spinBox_2.value(), 10))
        +    
        +    def _on_mode_changed(self):
        +        sender = self.sender()
        +        mode = sender.currentText()
        +        if mode == 'leak':
        +            self.label_28.show()
        +            self.label_29.hide()
        +            self.label_30.hide()
        +            self.spinBox.hide()
        +            self.spinBox_2.hide()
        +            self.spinBox_7.show()
        +        if mode == 'bar':
        +            self.label_28.hide()
        +            self.label_28.show()
        +            self.label_30.show()
        +            self.spinBox.show()
        +            self.spinBox_2.show()
        +            self.spinBox_7.hide()
        +        if mode == 'peak':
        +            self.label_28.hide()
        +            self.label_28.hide()
        +            self.label_30.hide()
        +            self.spinBox.hide()
        +            self.spinBox_2.hide()
        +            self.spinBox_7.hide()
        +    
        +    def _on_restart_scan(self):
        +        try:
        +            self.actionObservers({'restart_scan' : []})
        +        except Exception as ex:
        +            logger.exception(ex)
        +    
        +    def setEnableControls(self, state):
        +        self.label_29.setEnabled(state)
        +        self.label_30.setEnabled(state)
        +        self.label_28.setEnabled(state)
        +        self.label_32.setEnabled(state)
        +        self.spinBox.setEnabled(state)
        +        self.spinBox_7.setEnabled(state)
        +        self.spinBox_2.setEnabled(state)
        +        self.comboBox.setEnabled(state)
        +        
        +        self.label_33.setEnabled(state)
        +        self.label_34.setEnabled(state)
        +        self.label_35.setEnabled(state)
        +        self.label_36.setEnabled(state)
        +        self.spinBox_3.setEnabled(state)
        +        self.spinBox_4.setEnabled(state)
        +        self.spinBox_5.setEnabled(state)
        +        self.spinBox_6.setEnabled(state)
        +    
        +    def _on_toggle_scan(self):
        +        # sender = self.sender()
        +        
        +        # scan_type = self.comboBox_2.currentText()
        +        name = datetime.now().strftime("%m%d%Y%H%M%S")
        +        # if scan_type == 'bar':
        +        start_mass = self.spinBox.value()
        +        end_mass = self.spinBox_2.value()
        +        filter_mode = self.comboBox.currentText()
        +        accuracy = self.spinBox_3.value()
        +        e_gain_index = self.spinBox_4.value()
        +        source_index = self.spinBox_5.value()
        +        detector_index = self.spinBox_6.value()
        +        num_scans = self.spinBox_8.value()
        +        try:
        +            if 'Start' in self.pushButton_2.text():
        +                self.pushButton_2.setText("Stop scan")
        +                self.actionObservers({'toggle_scan' : [False]})
        +                self.actionObservers({'add_bar_chart' : [name,
        +                                                        start_mass,
        +                                                        end_mass,
        +                                                        filter_mode,
        +                                                        accuracy,
        +                                                        e_gain_index,
        +                                                        source_index,
        +                                                        detector_index]})
        +                
        +                logger.info(f'Starting scan {name}, Start mass: {start_mass}, End mass: {end_mass},')
        +                self.actionObservers({'toggle_scan' : [True, num_scans]})
        +            elif 'Stop' in self.pushButton_2.text():
        +                self.pushButton_2.setText("Start scan")
        +                logger.info(f'Stopping scan {name}, Start mass: {start_mass}, End mass: {end_mass},')
        +                self.actionObservers({'toggle_scan' : [False]})
        +        except Exception as ex:
        +            logger.exception(ex)
        +                
        +    def _on_toggle_filament(self):
        +        sender = self.sender()
        +        try:
        +            if 'off' in sender.text():
        +                logger.info("Turning on filament")
        +                self.actionObservers({'toggle_filament' : [False]})
        +            elif 'on' in sender.text():
        +                logger.info("Turning off filament")
        +                self.actionObservers({'toggle_filament' : [True]})
        +        except Exception as ex:
        +            logger.exception(ex)
        +    
        +    def _on_new_running_signal(self, monitoring_info):
        +        if monitoring_info:
        +            self.label_27.setText('True')
        +            self.pushButton_2.setText("Stop scan")
        +            self.pushButton_3.setEnabled(True)
        +            self.setEnableControls(False)
        +        else:
        +            if 'Stop' in self.pushButton_2.text():
        +                if self.checkbox.isChecked():
        +                    nScans = self.spinBox_8.value()
        +                    self.actionObservers({'resume_scan' : [nScans]})
        +            else:
        +                self.label_27.setText('False')
        +                self.pushButton_2.setText("Start scan")
        +                self.pushButton_3.setEnabled(False)
        +                self.setEnableControls(True)
        +                self.barRingBuffer = [np.nan] * 200
        +    
        +    def _on_new_scan_status_signal(self, monitoring_info):
        +        
        +        if 'Running' in monitoring_info:
        +            running = monitoring_info['Running']
        +            
        +            if running:
        +                self.pushButton_2.setText("Stop scan")
        +                self.pushButton_3.setEnabled(True)
        +            else:
        +                self.pushButton_2.setText("Start scan")
        +                self.pushButton_3.setEnabled(False)
        +                try:
        +                    self.actionObservers({'toggle_scan' : [False]})
        +                except Exception as ex:
        +                    logger.exectpion(ex)
        +            
        +
        +        self.label_20.setText(monitoring_info['StartMass'])
        +        self.label_21.setText(monitoring_info['EndMass'])
        +        self.label_22.setText(monitoring_info['FilterMode'])
        +        self.label_23.setText(monitoring_info['Accuracy'])
        +        self.label_24.setText(monitoring_info['EGainIndex'])
        +        self.label_25.setText(monitoring_info['SourceIndex'])
        +        self.label_26.setText(monitoring_info['DetectorIndex'])
        +        self.label_27.setText(str(monitoring_info['Running']))
        +    
        +    def _on_new_data_signal(self, monitoring_info):
        +        if monitoring_info != []:
        +            for i, value in enumerate(monitoring_info):
        +                if value != 0:
        +                    self.barRingBuffer[i] = value
        +            
        +            x = range(1, len(self.barRingBuffer)+1)
        +            self.canvas.ax.clear()
        +            self.canvas.ax.set_ylabel("Pressure")
        +            self.canvas.ax.set_yscale("log")
        +            self.canvas.ax.grid(True, which='major')
        +            self.canvas.ax.grid(True, which='minor', linestyle='--')
        +            self.canvas.ax.set_ylim((1E-9, 1E-4))
        +            self.canvas.ax.set_xlim((1, self.spinBox_2.value()))
        +            self.canvas.ax.bar(x, self.barRingBuffer, align='center')
        +            self.canvas.draw()
        +
        +    def _on_new_info_signal(self, monitoring_info):
        +        self.label_7.setText(monitoring_info['Name'])
        +        self.label_8.setText(monitoring_info['SerialNumber'])
        +        self.label_9.setText(monitoring_info['UserApplication'])
        +        self.label_10.setText(monitoring_info['UserAddress'])
        +        self.label_11.setText(monitoring_info['State'])
        +        self.label_12.setText(monitoring_info['ProductID'])
        +        self.label_13.setText(monitoring_info['DetectorType'])
        +        self.label_14.setText(monitoring_info['TotalPressureGauge'])
        +        self.label_15.setText(monitoring_info['FilamentType'])
        +        self.label_16.setText(monitoring_info['SensorType'])
        +        self.label_17.setText(monitoring_info['MaxMass'])
        +    
        +    def _on_new_filament_signal(self, monitoring_info):
        +        self.label_2.setText(monitoring_info['SummaryState'])
        +        
        +        if monitoring_info['SummaryState'] == 'OFF':
        +            self.pushButton.setText('Turn on filament')
        +        else:
        +            self.pushButton.setText('Turn off filament')
        +
        +        self.label_3.setText(monitoring_info['ActiveFilament'])
        +        self.label_4.setText(monitoring_info['MaxOnTime'])
        +        self.label_5.setText(monitoring_info['OnTimeRemaining'])
        +
        +

        Ancestors

        +
          +
        • PyQt5.QtWidgets.QMainWindow
        • +
        • PyQt5.QtWidgets.QWidget
        • +
        • PyQt5.QtCore.QObject
        • +
        • sip.wrapper
        • +
        • PyQt5.QtGui.QPaintDevice
        • +
        • sip.simplewrapper
        • +
        • Observable
        • +
        +

        Methods

        +
        +
        +def initUI(self) +
        +
        +
        +
        + +Expand source code + +
        def initUI(self):
        +    self.centerWidget = QWidget(self)
        +    
        +    self.gridLayout = QGridLayout(self.centerWidget)
        +    
        +    # Filament groupbox
        +    self.groupBox   = QGroupBox()
        +    self.verticalLayout = QVBoxLayout()
        +    self.label = QLabel("<b>Filament information</b>")
        +    self.formLayout = QFormLayout()
        +    self.label_2 = QLabel("")
        +    self.label_3 = QLabel("")
        +    self.label_4 = QLabel("")
        +    self.label_5 = QLabel("")
        +    self.formLayout.addRow(QLabel("Summary state"), self.label_2)
        +    self.formLayout.addRow(QLabel("Active filament"), self.label_3)
        +    self.formLayout.addRow(QLabel("Max on time"), self.label_4)
        +    self.verticalLayout.addWidget(self.label)
        +    self.verticalLayout.addLayout(self.formLayout)
        +    self.groupBox.setLayout(self.verticalLayout)
        +    self.gridLayout.addWidget(self.groupBox, 0, 0, 1 ,1)
        +    self.groupBox.setMinimumWidth(300)
        +    self.groupBox.setMaximumWidth(400)
        +    self.groupBox.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        +    
        +    
        +    # Sensor groupbox
        +    self.groupBox_2 = QGroupBox()
        +    self.verticalLayout_2 = QVBoxLayout()
        +    self.label_6 = QLabel("<b>Sensor information</b>")
        +    self.formLayout_2 = QFormLayout()
        +    self.label_7 = QLabel("")
        +    self.label_8 = QLabel("")
        +    self.label_9 = QLabel("")
        +    self.label_10 = QLabel("")
        +    self.label_11 = QLabel("")
        +    self.label_12 = QLabel("")
        +    self.label_13 = QLabel("")
        +    self.label_14 = QLabel("")
        +    self.label_15 = QLabel("")
        +    self.label_16 = QLabel("")
        +    self.label_17 = QLabel("")
        +    self.formLayout_2.addRow(QLabel("Name"), self.label_7)
        +    self.formLayout_2.addRow(QLabel("Serial number"), self.label_8)
        +    self.formLayout_2.addRow(QLabel("User application"), self.label_9)
        +    self.formLayout_2.addRow(QLabel("User address"), self.label_10)
        +    self.formLayout_2.addRow(QLabel("State"), self.label_11)
        +    self.formLayout_2.addRow(QLabel("Product ID"), self.label_12)
        +    self.formLayout_2.addRow(QLabel("Detector type"), self.label_13)
        +    self.formLayout_2.addRow(QLabel("Total pressure gauge"), self.label_14)
        +    self.formLayout_2.addRow(QLabel("Filament type"), self.label_15)
        +    self.formLayout_2.addRow(QLabel("Sensor type"), self.label_16)
        +    self.formLayout_2.addRow(QLabel("Max mass"), self.label_17)
        +    self.verticalLayout_2.addWidget(self.label_6)
        +    self.verticalLayout_2.addLayout(self.formLayout_2)
        +    self.groupBox_2.setLayout(self.verticalLayout_2)
        +    self.gridLayout.addWidget(self.groupBox_2, 1, 0, 1 ,1)
        +    self.groupBox_2.setMinimumWidth(300)
        +    self.groupBox_2.setMaximumWidth(400)
        +    self.groupBox_2.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        +    
        +    # Scan status
        +    self.groupBox_3 = QGroupBox()
        +    self.verticalLayout_3 = QVBoxLayout()
        +    self.label_18 = QLabel("<b>Scan information</b>")
        +    self.formLayout_6 = QFormLayout()
        +    self.label_20 = QLabel("")
        +    self.label_21 = QLabel("")
        +    self.label_22 = QLabel("")
        +    self.label_23 = QLabel("")
        +    self.label_24 = QLabel("")
        +    self.label_25 = QLabel("")
        +    self.label_26 = QLabel("")
        +    self.label_27 = QLabel("")
        +    self.formLayout_6.addRow(QLabel("Start mass"), self.label_20)
        +    self.formLayout_6.addRow(QLabel("End mass"), self.label_21)
        +    self.formLayout_6.addRow(QLabel("Filter mode"), self.label_22)
        +    self.formLayout_6.addRow(QLabel("Accuracy"), self.label_23)
        +    self.formLayout_6.addRow(QLabel("E Gain Index"), self.label_24)
        +    self.formLayout_6.addRow(QLabel("Source index"), self.label_25)
        +    self.formLayout_6.addRow(QLabel("Detector index"), self.label_26)
        +    self.formLayout_6.addRow(QLabel("Running"), self.label_27)
        +    self.verticalLayout_3.addWidget(self.label_18)
        +    self.verticalLayout_3.addLayout(self.formLayout_6)
        +    self.groupBox_3.setLayout(self.verticalLayout_3)
        +    self.gridLayout.addWidget(self.groupBox_3, 2, 0 ,1, 1)
        +    self.groupBox_3.setMinimumWidth(300)
        +    self.groupBox_3.setMaximumWidth(400)
        +    self.groupBox_3.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        +    # spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        +
        +    
        +    # Plot navigation groupbox
        +    self.groupBox_4 = QGroupBox()
        +    self.horizontalLayout = QHBoxLayout()
        +    self.formLayout_3 = QFormLayout()
        +    self.label_29 = QLabel("Start mass:")
        +    self.spinBox = QSpinBox()
        +    self.spinBox.setMinimum(1)
        +    self.spinBox.setMaximum(200)
        +    self.spinBox.setValue(1)
        +    self.spinBox.setMaximumWidth(150)
        +    self.spinBox.valueChanged.connect(self._set_plot_x_axis)
        +    self.label_30 = QLabel("End mass:")
        +    self.spinBox_2 = QSpinBox()
        +    self.spinBox_2.setMinimum(2)
        +    self.spinBox_2.setMaximum(200)
        +    self.spinBox_2.setValue(100)
        +    self.spinBox_2.setMaximumWidth(150)
        +    self.spinBox.valueChanged.connect(self._set_plot_x_axis)
        +    self.comboBox = QComboBox()
        +    self.comboBox.addItems(['PeakCenter', 'PeakMax', 'PeakAverage'])
        +    self.comboBox.setMaximumWidth(150)
        +    
        +    self.label_28 = QLabel("Mass:")
        +    self.spinBox_7 = QSpinBox()
        +    self.spinBox_7.setMinimum(1)
        +    self.spinBox_7.setMaximum(200)
        +    self.spinBox_7.setValue(4)
        +    self.spinBox_7.setMaximumWidth(150)
        +    self.label_28.hide()
        +    self.spinBox_7.hide()
        +    self.label_32 = QLabel("Filter mode:")
        +    self.formLayout_3.addRow(self.label_29, self.spinBox)
        +    self.formLayout_3.addRow(self.label_30, self.spinBox_2)
        +    self.formLayout_3.addRow(self.label_28, self.spinBox_7)
        +    self.formLayout_3.addRow(self.label_32, self.comboBox)
        +    self.horizontalLayout.addLayout(self.formLayout_3)
        +    
        +    self.formLayout_4 = QFormLayout()
        +    self.label_33 = QLabel("Accuracy:")
        +    self.spinBox_3 = QSpinBox()
        +    self.spinBox_3.setMinimum(0)
        +    self.spinBox_3.setMaximum(10)
        +    self.spinBox_3.setValue(5)
        +    self.spinBox_3.setMinimumWidth(150)
        +    self.label_34 = QLabel("EGain index:")
        +    self.spinBox_4 = QSpinBox()
        +    self.spinBox_4.setMinimumWidth(150)
        +    self.label_35 = QLabel("Source index:")
        +    self.spinBox_5 = QSpinBox()
        +    self.spinBox_5.setMinimumWidth(150)
        +    self.label_36 = QLabel("Detector index:")
        +    self.spinBox_6 = QSpinBox()
        +    self.spinBox_6.setMinimumWidth(150)
        +    self.formLayout_4.addRow(self.label_33, self.spinBox_3)
        +    self.formLayout_4.addRow(self.label_34, self.spinBox_4)
        +    self.formLayout_4.addRow(self.label_35, self.spinBox_5)
        +    self.formLayout_4.addRow(self.label_36, self.spinBox_6)
        +    self.horizontalLayout.addLayout(self.formLayout_4)
        +    
        +    spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        +    self.horizontalLayout.addItem(spacerItem)
        +    
        +    self.formLayout_5 = QFormLayout()
        +    # self.comboBox_2 = QComboBox()
        +    # self.comboBox_2.addItems(['bar', 'peak', 'leak'])
        +    # self.comboBox_2.currentTextChanged.connect(self._on_mode_changed)
        +    # self.formLayout_5.addRow(QLabel("Measurement mode:"), self.comboBox_2)
        +    self.checkbox = QCheckBox("Continous measurement")
        +    self.formLayout_5.addRow(None, self.checkbox)
        +    
        +    
        +    self.pushButton = QPushButton("Turn off filament")
        +    self.pushButton.clicked.connect(self._on_toggle_filament)
        +    self.formLayout_5.addRow(QLabel(""), self.pushButton)
        +    self.pushButton_2 = QPushButton("Start scanning")
        +    self.pushButton_2.clicked.connect(self._on_toggle_scan)
        +    self.spinBox_8 = QSpinBox()
        +    self.spinBox_8.setMinimum(0)
        +    self.spinBox_8.setMaximum(1000)
        +    self.spinBox_8.setValue(10)
        +    self.formLayout_5.addRow(self.spinBox_8, self.pushButton_2)
        +    self.pushButton_3 = QPushButton("Restart scan")
        +    self.pushButton_3.clicked.connect(self._on_restart_scan)
        +    self.formLayout_5.addRow(QLabel(""), self.pushButton_3)
        +    self.horizontalLayout.addLayout(self.formLayout_5)
        +    
        +    self.groupBox_4.setLayout(self.horizontalLayout)
        +    self.groupBox_4.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum)
        +    self.gridLayout.addWidget(self.groupBox_4, 3, 0, 1, 3)
        +    
        +    # Plot widget
        +    self.canvas = mPlotCanvas()
        +    self.canvas.ax.set_xticks(range(self.spinBox.value(), self.spinBox_2.value(), 10))
        +    self.canvas.ax.grid(True, which='major')
        +    self.canvas.ax.grid(True, which='minor', linestyle='--')
        +    self.gridLayout.addWidget(self.canvas, 0, 1, 3, 2)
        +    self.canvas.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
        +    
        +    self.setCentralWidget(self.centerWidget)
        +
        +
        +
        +def setEnableControls(self, state) +
        +
        +
        +
        + +Expand source code + +
        def setEnableControls(self, state):
        +    self.label_29.setEnabled(state)
        +    self.label_30.setEnabled(state)
        +    self.label_28.setEnabled(state)
        +    self.label_32.setEnabled(state)
        +    self.spinBox.setEnabled(state)
        +    self.spinBox_7.setEnabled(state)
        +    self.spinBox_2.setEnabled(state)
        +    self.comboBox.setEnabled(state)
        +    
        +    self.label_33.setEnabled(state)
        +    self.label_34.setEnabled(state)
        +    self.label_35.setEnabled(state)
        +    self.label_36.setEnabled(state)
        +    self.spinBox_3.setEnabled(state)
        +    self.spinBox_4.setEnabled(state)
        +    self.spinBox_5.setEnabled(state)
        +    self.spinBox_6.setEnabled(state)
        +
        +
        +
        +
        +
        +class mPlotCanvas +
        +
        +

        The canvas the figure renders into.

        +

        Attributes

        +
        +
        figure : ~matplotlib.figure.Figure
        +
        A high-level figure instance.
        +
        +
        + +Expand source code + +
        class mPlotCanvas(Canvas):
        +    def __init__(self):
        +        super().__init__()
        +        
        +        # self.fig, self.ax = 
        +        self.fig = Figure()
        +        self.ax  = self.fig.add_subplot(111)
        +        Canvas.__init__(self, self.fig)
        +        Canvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding)
        +        Canvas.updateGeometry(self)
        +
        +

        Ancestors

        +
          +
        • matplotlib.backends.backend_qtagg.FigureCanvasQTAgg
        • +
        • matplotlib.backends.backend_agg.FigureCanvasAgg
        • +
        • matplotlib.backends.backend_qt.FigureCanvasQT
        • +
        • matplotlib.backend_bases.FigureCanvasBase
        • +
        • PyQt5.QtWidgets.QWidget
        • +
        • PyQt5.QtCore.QObject
        • +
        • sip.wrapper
        • +
        • PyQt5.QtGui.QPaintDevice
        • +
        • sip.simplewrapper
        • +
        +
        +
        +
        +
        + +
        + + + \ No newline at end of file diff --git a/docs/api/egse/vacuum/mks/index.html b/docs/api/egse/vacuum/mks/index.html index a28153b..b56326c 100644 --- a/docs/api/egse/vacuum/mks/index.html +++ b/docs/api/egse/vacuum/mks/index.html @@ -26,23 +26,27 @@

        Namespace egse.vacuum.mks

        Sub-modules

        -
        egse.vacuum.mks.vacscan
        +
        egse.vacuum.mks.evision
        -
        egse.vacuum.mks.vacscan_controller
        +
        egse.vacuum.mks.evision_cs
        -
        egse.vacuum.mks.vacscan_cs
        +
        egse.vacuum.mks.evision_devif
        -
        egse.vacuum.mks.vacscan_interface
        +
        egse.vacuum.mks.evision_interface
        -
        egse.vacuum.mks.vacscan_simulator
        +
        egse.vacuum.mks.evision_simulator
        +
        +
        +
        +
        egse.vacuum.mks.evision_ui
        @@ -68,11 +72,12 @@

        Index

      • Sub-modules

      diff --git a/docs/api/egse/vacuum/mks/vacscan_controller.html b/docs/api/egse/vacuum/mks/vacscan_controller.html deleted file mode 100644 index c88c116..0000000 --- a/docs/api/egse/vacuum/mks/vacscan_controller.html +++ /dev/null @@ -1,549 +0,0 @@ - - - - - - -egse.vacuum.mks.vacscan_controller API documentation - - - - - - - - - - - -
      -
      -
      -

      Module egse.vacuum.mks.vacscan_controller

      -
      -
      -
      - -Expand source code - -
      import logging
      -import time
      -from functools import partial
      -
      -import yaml
      -
      -from egse.serialdevice import SerialDevice
      -from egse.command import Command
      -from egse.settings import Settings
      -from egse.vacuum.mks.vacscan_interface import VacscanInterface, VacscanError
      -
      -logger = logging.getLogger(__name__)
      -
      -# Load the device settings from the global common-egse config file
      -DEVICE_SETTINGS = Settings.load("MKS Vacscan Controller")
      -
      -# Load the device protocol
      -DEVICE_PROTOCOL = Settings.load(filename='vacscan.yaml')['Commands']
      -
      -
      -class VacscanController(SerialDevice, VacscanInterface):
      -
      -    def __init__(self, port=None, baudrate=9600):
      -
      -        # Load device configuration from the common-egse global config file
      -        self._port     = DEVICE_SETTINGS.PORT if port is None else port
      -        self._baudrate = DEVICE_SETTINGS.BAUDRATE if baudrate is None else baudrate
      -
      -        # Create a dict of Command objects for each function
      -        self._commands = {}
      -        for name, items in DEVICE_PROTOCOL.items():
      -            if 'cmd' in items:
      -                self._commands[name] = Command(name, items['cmd'])
      -
      -        # Initialize the parent class with the port and baudrate
      -        super().__init__(port=self._port, baudrate=self._baudrate, terminator='\r\n')
      -        self.connect()
      -
      -    def reset(self):
      -        return self.query(self._commands['reset'].get_cmd_string(), check_response=False)
      -
      -    def enable_acknowledge(self):
      -        return self.query(self._commands['enable_acknowledge'].get_cmd_string())
      -
      -    def get_status(self):
      -        return self.query(self._commands['get_status'].get_cmd_string())
      -
      -    def read_data(self):
      -        return self.query(self._commands['read_data'].get_cmd_string())
      -
      -    def set_barchart(self):
      -        return self.query(self._commands['set_barchart'].get_cmd_string())
      -
      -    def enable_filament(self, index, enable):
      -        return self.query(self._commands['enable_filament'].get_cmd_string(index, enable))
      -
      -    def press_key(self, index, count):
      -        return self.query(self._commands['press_key'].get_cmd_string(index, count))
      -
      -    def set_scan_speed(self, value):
      -        return self.query(self._commands['set_scan_speed'].get_cmd_string(value))
      -
      -    def set_first_mass(self, value):
      -        return self.query(self._commands['set_first_mass'].get_cmd_string(value))
      -
      -    def set_mass_zoom(self, value):
      -        return self.query(self._commands['set_mass_zoom'].get_cmd_string(value))
      -
      -    def set_gain(self, value):
      -        return self.query(self._commands['set_gain'].get_cmd_string(value))
      -
      -    def set_time_interval(self, value):
      -        return self.query(self._commands['set_time_interval'].get_cmd_string(value))
      -
      -    def query(self, command, check_response=True):
      -        """ Override the parent class to do some error checking on the response. """
      -
      -        if not check_response:
      -            self.send_command(command)
      -
      -        else:
      -            response = super().query(command)
      -
      -            if len(response) == 0:
      -                raise VacscanError('No response received for command: {}'.format(command))
      -            elif response[-2:] != '\r\n':
      -                raise VacscanError('Invalid response terminators received: {}'.format(response))
      -            elif response[0] == 'N':
      -                raise VacscanError('NACK response received for command: {}'.format(command))
      -            elif response[0] != 'A':
      -                raise VacscanError('Invalid response received: {}'.format(response))
      -
      -            if len(response) > 3:
      -                return response[1:-2]
      -
      -        return None
      -
      -def main():
      -    dev = VacscanController(port='/dev/vacscan')
      -
      -    print(dev.get_status())
      -    # dev.set_gain(8)
      -    dev.set_mass_zoom(100)
      -    print(dev.get_status())
      -
      -    # dev.reset()
      -    # dev.enable_acknowledge()
      -    # dev.set_gain(7)
      -    # dev.set_time_interval(100)
      -    dev.set_barchart()
      -    # dev.enable_filament(1, 1)
      -
      -    while True:
      -        time.sleep(10)
      -        print(dev.read_data())
      -
      -
      -if __name__ == '__main__':
      -    main()
      -
      -
      -
      -
      -
      -
      -
      -

      Functions

      -
      -
      -def main() -
      -
      -
      -
      - -Expand source code - -
      def main():
      -    dev = VacscanController(port='/dev/vacscan')
      -
      -    print(dev.get_status())
      -    # dev.set_gain(8)
      -    dev.set_mass_zoom(100)
      -    print(dev.get_status())
      -
      -    # dev.reset()
      -    # dev.enable_acknowledge()
      -    # dev.set_gain(7)
      -    # dev.set_time_interval(100)
      -    dev.set_barchart()
      -    # dev.enable_filament(1, 1)
      -
      -    while True:
      -        time.sleep(10)
      -        print(dev.read_data())
      -
      -
      -
      -
      -
      -

      Classes

      -
      -
      -class VacscanController -(port=None, baudrate=9600) -
      -
      -

      Generic base class for serial based device interface classes.

      -

      This base class handles the serial connection.

      -
      - -Expand source code - -
      class VacscanController(SerialDevice, VacscanInterface):
      -
      -    def __init__(self, port=None, baudrate=9600):
      -
      -        # Load device configuration from the common-egse global config file
      -        self._port     = DEVICE_SETTINGS.PORT if port is None else port
      -        self._baudrate = DEVICE_SETTINGS.BAUDRATE if baudrate is None else baudrate
      -
      -        # Create a dict of Command objects for each function
      -        self._commands = {}
      -        for name, items in DEVICE_PROTOCOL.items():
      -            if 'cmd' in items:
      -                self._commands[name] = Command(name, items['cmd'])
      -
      -        # Initialize the parent class with the port and baudrate
      -        super().__init__(port=self._port, baudrate=self._baudrate, terminator='\r\n')
      -        self.connect()
      -
      -    def reset(self):
      -        return self.query(self._commands['reset'].get_cmd_string(), check_response=False)
      -
      -    def enable_acknowledge(self):
      -        return self.query(self._commands['enable_acknowledge'].get_cmd_string())
      -
      -    def get_status(self):
      -        return self.query(self._commands['get_status'].get_cmd_string())
      -
      -    def read_data(self):
      -        return self.query(self._commands['read_data'].get_cmd_string())
      -
      -    def set_barchart(self):
      -        return self.query(self._commands['set_barchart'].get_cmd_string())
      -
      -    def enable_filament(self, index, enable):
      -        return self.query(self._commands['enable_filament'].get_cmd_string(index, enable))
      -
      -    def press_key(self, index, count):
      -        return self.query(self._commands['press_key'].get_cmd_string(index, count))
      -
      -    def set_scan_speed(self, value):
      -        return self.query(self._commands['set_scan_speed'].get_cmd_string(value))
      -
      -    def set_first_mass(self, value):
      -        return self.query(self._commands['set_first_mass'].get_cmd_string(value))
      -
      -    def set_mass_zoom(self, value):
      -        return self.query(self._commands['set_mass_zoom'].get_cmd_string(value))
      -
      -    def set_gain(self, value):
      -        return self.query(self._commands['set_gain'].get_cmd_string(value))
      -
      -    def set_time_interval(self, value):
      -        return self.query(self._commands['set_time_interval'].get_cmd_string(value))
      -
      -    def query(self, command, check_response=True):
      -        """ Override the parent class to do some error checking on the response. """
      -
      -        if not check_response:
      -            self.send_command(command)
      -
      -        else:
      -            response = super().query(command)
      -
      -            if len(response) == 0:
      -                raise VacscanError('No response received for command: {}'.format(command))
      -            elif response[-2:] != '\r\n':
      -                raise VacscanError('Invalid response terminators received: {}'.format(response))
      -            elif response[0] == 'N':
      -                raise VacscanError('NACK response received for command: {}'.format(command))
      -            elif response[0] != 'A':
      -                raise VacscanError('Invalid response received: {}'.format(response))
      -
      -            if len(response) > 3:
      -                return response[1:-2]
      -
      -        return None
      -
      -

      Ancestors

      - -

      Methods

      -
      -
      -def enable_acknowledge(self) -
      -
      -
      -
      - -Expand source code - -
      def enable_acknowledge(self):
      -    return self.query(self._commands['enable_acknowledge'].get_cmd_string())
      -
      -
      -
      -def enable_filament(self, index, enable) -
      -
      -
      -
      - -Expand source code - -
      def enable_filament(self, index, enable):
      -    return self.query(self._commands['enable_filament'].get_cmd_string(index, enable))
      -
      -
      -
      -def get_status(self) -
      -
      -
      -
      - -Expand source code - -
      def get_status(self):
      -    return self.query(self._commands['get_status'].get_cmd_string())
      -
      -
      -
      -def press_key(self, index, count) -
      -
      -
      -
      - -Expand source code - -
      def press_key(self, index, count):
      -    return self.query(self._commands['press_key'].get_cmd_string(index, count))
      -
      -
      -
      -def query(self, command, check_response=True) -
      -
      -

      Override the parent class to do some error checking on the response.

      -
      - -Expand source code - -
      def query(self, command, check_response=True):
      -    """ Override the parent class to do some error checking on the response. """
      -
      -    if not check_response:
      -        self.send_command(command)
      -
      -    else:
      -        response = super().query(command)
      -
      -        if len(response) == 0:
      -            raise VacscanError('No response received for command: {}'.format(command))
      -        elif response[-2:] != '\r\n':
      -            raise VacscanError('Invalid response terminators received: {}'.format(response))
      -        elif response[0] == 'N':
      -            raise VacscanError('NACK response received for command: {}'.format(command))
      -        elif response[0] != 'A':
      -            raise VacscanError('Invalid response received: {}'.format(response))
      -
      -        if len(response) > 3:
      -            return response[1:-2]
      -
      -    return None
      -
      -
      -
      -def read_data(self) -
      -
      -
      -
      - -Expand source code - -
      def read_data(self):
      -    return self.query(self._commands['read_data'].get_cmd_string())
      -
      -
      -
      -def reset(self) -
      -
      -
      -
      - -Expand source code - -
      def reset(self):
      -    return self.query(self._commands['reset'].get_cmd_string(), check_response=False)
      -
      -
      -
      -def set_barchart(self) -
      -
      -
      -
      - -Expand source code - -
      def set_barchart(self):
      -    return self.query(self._commands['set_barchart'].get_cmd_string())
      -
      -
      -
      -def set_first_mass(self, value) -
      -
      -
      -
      - -Expand source code - -
      def set_first_mass(self, value):
      -    return self.query(self._commands['set_first_mass'].get_cmd_string(value))
      -
      -
      -
      -def set_gain(self, value) -
      -
      -
      -
      - -Expand source code - -
      def set_gain(self, value):
      -    return self.query(self._commands['set_gain'].get_cmd_string(value))
      -
      -
      -
      -def set_mass_zoom(self, value) -
      -
      -
      -
      - -Expand source code - -
      def set_mass_zoom(self, value):
      -    return self.query(self._commands['set_mass_zoom'].get_cmd_string(value))
      -
      -
      -
      -def set_scan_speed(self, value) -
      -
      -
      -
      - -Expand source code - -
      def set_scan_speed(self, value):
      -    return self.query(self._commands['set_scan_speed'].get_cmd_string(value))
      -
      -
      -
      -def set_time_interval(self, value) -
      -
      -
      -
      - -Expand source code - -
      def set_time_interval(self, value):
      -    return self.query(self._commands['set_time_interval'].get_cmd_string(value))
      -
      -
      -
      -

      Inherited members

      - -
      -
      -
      -
      - -
      - - - \ No newline at end of file diff --git a/docs/api/egse/vacuum/mks/vacscan_interface.html b/docs/api/egse/vacuum/mks/vacscan_interface.html deleted file mode 100644 index 903d998..0000000 --- a/docs/api/egse/vacuum/mks/vacscan_interface.html +++ /dev/null @@ -1,210 +0,0 @@ - - - - - - -egse.vacuum.mks.vacscan_interface API documentation - - - - - - - - - - - -
      -
      -
      -

      Module egse.vacuum.mks.vacscan_interface

      -
      -
      -
      - -Expand source code - -
      from egse.device import DeviceInterface
      -from egse.decorators import dynamic_interface
      -
      -
      -class VacscanError(Exception):
      -    """ Vacscan protocol errors. """
      -
      -
      -class VacscanInterface(DeviceInterface):
      -
      -    @dynamic_interface
      -    def get_status(self):
      -        return NotImplemented
      -
      -    @dynamic_interface
      -    def reset(self):
      -        return NotImplemented
      -
      -    @dynamic_interface
      -    def get_barchart(self):
      -        return NotImplemented
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -

      Classes

      -
      -
      -class VacscanError -(*args, **kwargs) -
      -
      -

      Vacscan protocol errors.

      -
      - -Expand source code - -
      class VacscanError(Exception):
      -    """ Vacscan protocol errors. """
      -
      -

      Ancestors

      -
        -
      • builtins.Exception
      • -
      • builtins.BaseException
      • -
      -
      -
      -class VacscanInterface -
      -
      -

      Generic interface for all device classes.

      -
      - -Expand source code - -
      class VacscanInterface(DeviceInterface):
      -
      -    @dynamic_interface
      -    def get_status(self):
      -        return NotImplemented
      -
      -    @dynamic_interface
      -    def reset(self):
      -        return NotImplemented
      -
      -    @dynamic_interface
      -    def get_barchart(self):
      -        return NotImplemented
      -
      -

      Ancestors

      - -

      Subclasses

      - -

      Methods

      -
      -
      -def get_barchart(self) -
      -
      -
      -
      - -Expand source code - -
      @dynamic_interface
      -def get_barchart(self):
      -    return NotImplemented
      -
      -
      -
      -def get_status(self) -
      -
      -
      -
      - -Expand source code - -
      @dynamic_interface
      -def get_status(self):
      -    return NotImplemented
      -
      -
      -
      -def reset(self) -
      -
      -
      -
      - -Expand source code - -
      @dynamic_interface
      -def reset(self):
      -    return NotImplemented
      -
      -
      -
      -

      Inherited members

      - -
      -
      -
      -
      - -
      - - - \ No newline at end of file diff --git a/docs/api/egse/vacuum/pfeiffer/acp40.html b/docs/api/egse/vacuum/pfeiffer/acp40.html index aeefda9..fc6967e 100644 --- a/docs/api/egse/vacuum/pfeiffer/acp40.html +++ b/docs/api/egse/vacuum/pfeiffer/acp40.html @@ -30,18 +30,18 @@

      Module egse.vacuum.pfeiffer.acp40

      from prometheus_client import Gauge -from egse.control import ControlServer from egse.command import ClientServerCommand +from egse.control import ControlServer from egse.protocol import CommandProtocol from egse.proxy import Proxy from egse.settings import Settings -from egse.zmq_ser import bind_address from egse.system import format_datetime -from egse.vacuum.pfeiffer.acp40_interface import Acp40Interface from egse.vacuum.pfeiffer.acp40_controller import Acp40Controller +from egse.vacuum.pfeiffer.acp40_interface import Acp40Interface from egse.vacuum.pfeiffer.acp40_simulator import Acp40Simulator +from egse.zmq_ser import bind_address from egse.zmq_ser import connect_address -from egse.serialdevice import SerialTimeoutException, SerialException + logger = logging.getLogger(__name__) DEVICE_SETTINGS = Settings.load(filename="acp40.yaml") diff --git a/docs/api/egse/vacuum/pfeiffer/acp40_controller.html b/docs/api/egse/vacuum/pfeiffer/acp40_controller.html index b1f35e8..16a7a0d 100644 --- a/docs/api/egse/vacuum/pfeiffer/acp40_controller.html +++ b/docs/api/egse/vacuum/pfeiffer/acp40_controller.html @@ -27,10 +27,10 @@

      Module egse.vacuum.pfeiffer.acp40_controller

      Expand source code
      import logging
      +
       from serial import SerialTimeoutException
       
       from egse.serialdevice import SerialDevice
      -from egse.command import Command
       from egse.settings import Settings
       from egse.vacuum.pfeiffer.acp40_interface import Acp40Interface
       
      diff --git a/docs/api/egse/vacuum/pfeiffer/acp40_cs.html b/docs/api/egse/vacuum/pfeiffer/acp40_cs.html
      index 46c8756..3dd22c3 100644
      --- a/docs/api/egse/vacuum/pfeiffer/acp40_cs.html
      +++ b/docs/api/egse/vacuum/pfeiffer/acp40_cs.html
      @@ -296,10 +296,15 @@ 

      Inherited members

      • ControlServer:
      • diff --git a/docs/api/egse/vacuum/pfeiffer/acp40_simulator.html b/docs/api/egse/vacuum/pfeiffer/acp40_simulator.html index a032c2e..7f4e32e 100644 --- a/docs/api/egse/vacuum/pfeiffer/acp40_simulator.html +++ b/docs/api/egse/vacuum/pfeiffer/acp40_simulator.html @@ -28,9 +28,7 @@

        Module egse.vacuum.pfeiffer.acp40_simulator

        import logging
         
        -from egse.settings import Settings
         from egse.simulator import Simulator
        -from egse.command import Command
         from egse.vacuum.pfeiffer.acp40_interface import Acp40Interface
         
         logger = logging.getLogger(__name__)
        diff --git a/docs/api/egse/vacuum/pfeiffer/tc400.html b/docs/api/egse/vacuum/pfeiffer/tc400.html
        index dd5a8df..de3f007 100644
        --- a/docs/api/egse/vacuum/pfeiffer/tc400.html
        +++ b/docs/api/egse/vacuum/pfeiffer/tc400.html
        @@ -31,12 +31,12 @@ 

        Module egse.vacuum.pfeiffer.tc400

        from prometheus_client import Gauge from egse.control import ControlServer +from egse.fdir.fdir_manager import FdirManagerProxy from egse.protocol import CommandProtocol from egse.proxy import Proxy from egse.settings import Settings from egse.zmq_ser import bind_address from egse.system import format_datetime -# from egse.fdir.fdir_manager import FdirManagerProxy from egse.vacuum.pfeiffer.tc400_interface import Tc400Command, Tc400Interface from egse.vacuum.pfeiffer.tc400_controller import Tc400Controller from egse.vacuum.pfeiffer.tc400_simulator import Tc400Simulator @@ -69,7 +69,6 @@

        Module egse.vacuum.pfeiffer.tc400

        self.build_device_method_lookup_table(self.dev) - # move to parent class? def get_bind_address(self): return bind_address( @@ -77,7 +76,6 @@

        Module egse.vacuum.pfeiffer.tc400

        self.control_server.get_commanding_port(), ) - def get_status(self): status_dict = super().get_status() @@ -86,10 +84,8 @@

        Module egse.vacuum.pfeiffer.tc400

        return status_dict - def get_housekeeping(self) -> dict: result = dict() - prev_err = None result["timestamp"] = format_datetime() try: @@ -100,22 +96,8 @@

        Module egse.vacuum.pfeiffer.tc400

        except Exception as e: LOGGER.warning(f'failed to get HK ({e})') - # with FdirManagerProxy() as fdir: - # fdir_code = 100010 - # fdir.signal_fdir(fdir_code) - # LOGGER.warning(f'asserted fdir signal: {fdir_code}') - return result - # if prev_err is not result[f"G{SITE_ID}_TC400_LAST_ERROR"]: - # LOGGER.info(f"Device has raised an error flag: {result[f"G{SITE_ID}_TC400_LAST_ERROR"]}") - # with FdirManagerProxy() as fdir: - # fdir_code = 1010 - # fdir.signal_fdir(fdir_code) - # LOGGER.warning(f'asserted fdir signal: {fdir_code}') - - prev_err = result['GSRON_TC400_LAST_ERROR'] - gauge_active_speed.set(result[f"GSRON_TC400_ACT_SPEED"]) gauge_drive_power.set(result[f"GSRON_TC400_POWER"]) gauge_motor_temperature.set(result[f"GSRON_TC400_MOTOR_TEMP"]) @@ -129,16 +111,7 @@

        Module egse.vacuum.pfeiffer.tc400

        def __init__(self): super().__init__( connect_address( - CTRL_SETTINGS.PROTOCOL, CTRL_SETTINGS.HOSTNAME, CTRL_SETTINGS.COMMANDING_PORT)) - - def connect_cs(self): - try: - super().connect_cs() - except Exception: - with FdirManagerProxy() as fdir: - fdir_code = 10010 - fdir.signal_fdir(fdir_code) - LOGGER.warning(f'asserted fdir signal: {fdir_code}')
        + CTRL_SETTINGS.PROTOCOL, CTRL_SETTINGS.HOSTNAME, CTRL_SETTINGS.COMMANDING_PORT))
    @@ -183,7 +156,6 @@

    Classes

    self.build_device_method_lookup_table(self.dev) - # move to parent class? def get_bind_address(self): return bind_address( @@ -191,7 +163,6 @@

    Classes

    self.control_server.get_commanding_port(), ) - def get_status(self): status_dict = super().get_status() @@ -200,10 +171,8 @@

    Classes

    return status_dict - def get_housekeeping(self) -> dict: result = dict() - prev_err = None result["timestamp"] = format_datetime() try: @@ -214,22 +183,8 @@

    Classes

    except Exception as e: LOGGER.warning(f'failed to get HK ({e})') - # with FdirManagerProxy() as fdir: - # fdir_code = 100010 - # fdir.signal_fdir(fdir_code) - # LOGGER.warning(f'asserted fdir signal: {fdir_code}') - return result - # if prev_err is not result[f"G{SITE_ID}_TC400_LAST_ERROR"]: - # LOGGER.info(f"Device has raised an error flag: {result[f"G{SITE_ID}_TC400_LAST_ERROR"]}") - # with FdirManagerProxy() as fdir: - # fdir_code = 1010 - # fdir.signal_fdir(fdir_code) - # LOGGER.warning(f'asserted fdir signal: {fdir_code}') - - prev_err = result['GSRON_TC400_LAST_ERROR'] - gauge_active_speed.set(result[f"GSRON_TC400_ACT_SPEED"]) gauge_drive_power.set(result[f"GSRON_TC400_POWER"]) gauge_motor_temperature.set(result[f"GSRON_TC400_MOTOR_TEMP"]) @@ -287,16 +242,7 @@

    Inherited members

    def __init__(self): super().__init__( connect_address( - CTRL_SETTINGS.PROTOCOL, CTRL_SETTINGS.HOSTNAME, CTRL_SETTINGS.COMMANDING_PORT)) - - def connect_cs(self): - try: - super().connect_cs() - except Exception: - with FdirManagerProxy() as fdir: - fdir_code = 10010 - fdir.signal_fdir(fdir_code) - LOGGER.warning(f'asserted fdir signal: {fdir_code}')
    + CTRL_SETTINGS.PROTOCOL, CTRL_SETTINGS.HOSTNAME, CTRL_SETTINGS.COMMANDING_PORT))

    Ancestors

      diff --git a/docs/api/egse/vacuum/pfeiffer/tc400_controller.html b/docs/api/egse/vacuum/pfeiffer/tc400_controller.html index ccd2047..c0669f4 100644 --- a/docs/api/egse/vacuum/pfeiffer/tc400_controller.html +++ b/docs/api/egse/vacuum/pfeiffer/tc400_controller.html @@ -27,12 +27,8 @@

      Module egse.vacuum.pfeiffer.tc400_controller

      Expand source code
      import logging
      -import time
      -from functools import partial
      -from copy import deepcopy
       
       from egse.serialdevice import SerialDevice
      -from egse.command import Command
       from egse.settings import Settings
       from egse.vacuum.pfeiffer.tc400_interface import Tc400Interface, Tc400Command
       
      diff --git a/docs/api/egse/vacuum/pfeiffer/tc400_cs.html b/docs/api/egse/vacuum/pfeiffer/tc400_cs.html
      index 561d6f2..de3bb97 100644
      --- a/docs/api/egse/vacuum/pfeiffer/tc400_cs.html
      +++ b/docs/api/egse/vacuum/pfeiffer/tc400_cs.html
      @@ -296,10 +296,15 @@ 

      Inherited members

      + return "TM 700 (simulator)" + + def get_last_error(self): + return 0xFF + + def get_active_speed(self): + return 0 + + def get_drive_power(self): + return 0 + + def get_motor_temperature(self): + return 0
    @@ -80,7 +91,19 @@

    Classes

    return True def get_idn(self): - return "TM 700 (simulator)" + return "TM 700 (simulator)" + + def get_last_error(self): + return 0xFF + + def get_active_speed(self): + return 0 + + def get_drive_power(self): + return 0 + + def get_motor_temperature(self): + return 0

    Ancestors

      @@ -92,6 +115,32 @@

      Ancestors

    Methods

    +
    +def get_active_speed(self) +
    +
    +
    +
    + +Expand source code + +
    def get_active_speed(self):
    +    return 0
    +
    +
    +
    +def get_drive_power(self) +
    +
    +
    +
    + +Expand source code + +
    def get_drive_power(self):
    +    return 0
    +
    +
    def get_idn(self)
    @@ -105,6 +154,32 @@

    Methods

    return "TM 700 (simulator)" +
    +def get_last_error(self) +
    +
    +
    +
    + +Expand source code + +
    def get_last_error(self):
    +    return 0xFF
    +
    +
    +
    +def get_motor_temperature(self) +
    +
    +
    +
    + +Expand source code + +
    def get_motor_temperature(self):
    +    return 0
    +
    +

    Inherited members

    diff --git a/docs/api/egse/vacuum/pfeiffer/tpg261.html b/docs/api/egse/vacuum/pfeiffer/tpg261.html index 4b91d92..eb0f998 100644 --- a/docs/api/egse/vacuum/pfeiffer/tpg261.html +++ b/docs/api/egse/vacuum/pfeiffer/tpg261.html @@ -36,7 +36,6 @@

    Module egse.vacuum.pfeiffer.tpg261

    from egse.settings import Settings from egse.zmq_ser import bind_address from egse.system import format_datetime -# from egse.fdir.fdir_manager import FdirManagerProxy from egse.vacuum.pfeiffer.tpg261_interface import Tpg261Command, Tpg261Interface from egse.vacuum.pfeiffer.tpg261_controller import Tpg261Controller from egse.vacuum.pfeiffer.tpg261_simulator import Tpg261Simulator diff --git a/docs/api/egse/vacuum/pfeiffer/tpg261_cs.html b/docs/api/egse/vacuum/pfeiffer/tpg261_cs.html index 44872f0..583b16b 100644 --- a/docs/api/egse/vacuum/pfeiffer/tpg261_cs.html +++ b/docs/api/egse/vacuum/pfeiffer/tpg261_cs.html @@ -296,10 +296,15 @@

    Inherited members

    • ControlServer:
    • diff --git a/docs/api/egse/vacuum/pfeiffer/tpg261_interface.html b/docs/api/egse/vacuum/pfeiffer/tpg261_interface.html index ce15e38..f3c4534 100644 --- a/docs/api/egse/vacuum/pfeiffer/tpg261_interface.html +++ b/docs/api/egse/vacuum/pfeiffer/tpg261_interface.html @@ -26,8 +26,7 @@

      Module egse.vacuum.pfeiffer.tpg261_interface

      Expand source code -
      from calendar import day_abbr, day_name
      -from egse.device import DeviceInterface
      +
      from egse.device import DeviceInterface
       from egse.decorators import dynamic_interface
       from egse.command import ClientServerCommand
       
      diff --git a/docs/api/egse/vacuum/pfeiffer/tpg261_simulator.html b/docs/api/egse/vacuum/pfeiffer/tpg261_simulator.html
      index cc90962..d1aad12 100644
      --- a/docs/api/egse/vacuum/pfeiffer/tpg261_simulator.html
      +++ b/docs/api/egse/vacuum/pfeiffer/tpg261_simulator.html
      @@ -30,7 +30,6 @@ 

      Module egse.vacuum.pfeiffer.tpg261_simulator

      from egse.settings import Settings from egse.simulator import Simulator -from egse.command import Command from egse.vacuum.pfeiffer.tpg261_interface import Tpg261Interface logger = logging.getLogger(__name__) diff --git a/docs/api/egse/version.html b/docs/api/egse/version.html index f0981dc..c08ca6f 100644 --- a/docs/api/egse/version.html +++ b/docs/api/egse/version.html @@ -26,9 +26,8 @@

      Module egse.version

      This module defines the version for the Common-EGSE release. Whenever a version number or the release number is needed, this module shall be imported. The version and release number is then available as:

      -
      import egse.version
      -
      -print(f"version = {egse.version.VERSION}")
      +
      >>> import egse.version
      +>>> print(f"version = {egse.version.VERSION}")
       

      The actual numbers are updated for each release in the settings.yaml configuration file.

      @@ -40,49 +39,174 @@

      Module egse.version

      or the release number is needed, this module shall be imported. The version and release number is then available as: - import egse.version - - print(f"version = {egse.version.VERSION}") + >>> import egse.version + >>> print(f"version = {egse.version.VERSION}") The actual numbers are updated for each release in the `settings.yaml` configuration file. """ +from __future__ import annotations + +# WARNING: Make sure you are not importing any `egse` packages at the module level. +# This module is magically loaded by pip to determine the VERSION number before the +# package has been installed (see pyproject.py). +# Any imports from an 'egse' package will result in a ModuleNotFound error. + +import os import subprocess from pathlib import Path -try: - from egse.settings import Settings +HERE = Path(__file__).parent.resolve() - settings = Settings.load("Common-EGSE") - VERSION = settings.VERSION -except ModuleNotFoundError: - VERSION = "unknown version" +__all__ = [ + "get_version_installed", + "get_version_from_git", + "get_version_from_settings", + "VERSION", +] -if __name__ == "__main__": +def get_version_from_settings_file_raw(group_name: str, location: Path | str = None) -> str: + """ + Reads the VERSION field from the `settings.yaml` file in raw mode, meaning the file + is not read using the PyYAML module, but using the `readline()` function of the file + descriptor. - import rich + Args: + group_name: major group name that contains the VERSION field, i.e. Common-EGSE or PLATO_TEST_SCRIPTS. + location: the location of the `settings.yaml` file or None in which case the location of this file is used. + + Raises: + A RuntimeError when the group_name is incorrect and unknown or the VERSION field is not found. + + Returns: + The version from the `settings.yaml` file as a string. + """ + basedir = location or os.path.dirname(__file__) + + with open(os.path.join(basedir, "settings.yaml"), mode="r") as yaml_fd: + line = yaml_fd.readline() + if not line.startswith(group_name): + raise RuntimeError(f"Incompatible format for the settings.yaml file, should start with '{group_name}'") + + line = yaml_fd.readline().lstrip() + if not line.startswith("VERSION"): + raise RuntimeError("Incompatible format for the settings.yaml file, no VERSION found.") + _, version = line.split(":") + + # remove possible trailing comment starting with '#' + version, *_ = version.split("#") + version = version.strip() + + return version + + +def get_version_from_settings(group_name: str, location: Path = None): + """ + Reads the VERSION field from the `settings.yaml` file. This function first tries to load the proper Settings + and Group and if that fails uses the raw method. + + Args: + group_name: major group name that contains the VERSION field, i.e. Common-EGSE or PLATO_TEST_SCRIPTS. + location: the location of the `settings.yaml` file or None in which case the location of this file is used. + + Raises: + A RuntimeError when the group_name is incorrect and unknown or the VERSION field is not found. + + Returns: + The version from the `settings.yaml` file as a string. + """ + from egse.settings import Settings, SettingsError + + try: + settings = Settings.load(group_name, location=location) + version = settings.VERSION + except (ModuleNotFoundError, SettingsError): + version = get_version_from_settings_file_raw(group_name, location=location) + + return version + + +def get_version_from_git(location: str = None): + """ + Returns the Git version number for the repository at the given location. + + The returned string has the following format: YYYY.X.Y+REPO.TH-N-HASH, where: + + * YYYY is the year + * X is the major version number and equal to the week number of the release + * Y is the minor version patch number + * REPO is the name of the repository, i.e. CGSE or TS + * TH is the name of the test house, i.e. CSL1, CSL2, IAS, INTA, SRON + * N is the number of commits since the release + * HASH is the Git hash number of the commit + Args: + location: The absolute path of the root or a sub-folder of the repo. + + Returns: + The Git version number. + """ from egse.system import chdir - with chdir(Path(__file__).parent): + location = location or Path().cwd() + + with chdir(location): try: std_out = subprocess.check_output( - ["git", "describe", "--tags", "--long"], stderr=subprocess.PIPE) - git_version = std_out.strip().decode("ascii") + ["git", "describe", "--tags", "--long", "--always"], stderr=subprocess.PIPE + ) + version = std_out.strip().decode("ascii") + if "cgse" not in version.lower() and "ts" not in version.lower(): + version = None except subprocess.CalledProcessError as exc: - git_version = None + version = None + + return version + + +def get_version_installed(package_name: str) -> str: + """ + Returns the version that is installed, i.e. read from the metadata in the import lib. + + Args: + package_name: the name of the installed package, e.g. cgse or cgse-ts + + Returns: + The version of the installed repo. + """ + from egse.system import chdir + + with chdir(Path(__file__).parent): + from importlib.metadata import version, PackageNotFoundError + try: - from importlib.metadata import version, PackageNotFoundError - installed_version = version("Common-EGSE") + version = version(package_name) except PackageNotFoundError as exc: - installed_version = None + version = None + + return version + + +VERSION = get_version_from_settings_file_raw("Common-EGSE", location=HERE) + +# The __PYPI_VERSION__ is the version number that will be used for the installation. +# The version will appear in PyPI and also as the `installed version`. + +__PYPI_VERSION__ = VERSION.split('+')[0] - if VERSION and VERSION != installed_version: + +if __name__ == "__main__": + import rich + from egse.system import get_module_location + + if VERSION: rich.print(f"CGSE version in Settings: [bold default]{VERSION}[/]") - if git_version: + + if git_version := get_version_from_git(location=get_module_location("egse")): rich.print(f"CGSE git version = [bold default]{git_version}[/]") - if installed_version: + + if installed_version := get_version_installed("cgse"): rich.print(f"CGSE installed version = [bold default]{installed_version}[/]")
    @@ -91,6 +215,159 @@

    Module egse.version

    +

    Functions

    +
    +
    +def get_version_from_git(location: str = None) +
    +
    +

    Returns the Git version number for the repository at the given location.

    +

    The returned string has the following format: YYYY.X.Y+REPO.TH-N-HASH, where:

    +
      +
    • YYYY is the year
    • +
    • X is the major version number and equal to the week number of the release
    • +
    • Y is the minor version patch number
    • +
    • REPO is the name of the repository, i.e. CGSE or TS
    • +
    • TH is the name of the test house, i.e. CSL1, CSL2, IAS, INTA, SRON
    • +
    • N is the number of commits since the release
    • +
    • HASH is the Git hash number of the commit
    • +
    +

    Args

    +
    +
    location
    +
    The absolute path of the root or a sub-folder of the repo.
    +
    +

    Returns

    +

    The Git version number.

    +
    + +Expand source code + +
    def get_version_from_git(location: str = None):
    +    """
    +    Returns the Git version number for the repository at the given location.
    +
    +    The returned string has the following format: YYYY.X.Y+REPO.TH-N-HASH, where:
    +
    +    * YYYY is the year
    +    * X is the major version number and equal to the week number of the release
    +    * Y is the minor version patch number
    +    * REPO is the name of the repository, i.e. CGSE or TS
    +    * TH is the name of the test house, i.e. CSL1, CSL2, IAS, INTA, SRON
    +    * N is the number of commits since the release
    +    * HASH is the Git hash number of the commit
    +
    +    Args:
    +        location: The absolute path of the root or a sub-folder of the repo.
    +
    +    Returns:
    +        The Git version number.
    +    """
    +    from egse.system import chdir
    +
    +    location = location or Path().cwd()
    +
    +    with chdir(location):
    +        try:
    +            std_out = subprocess.check_output(
    +                ["git", "describe", "--tags", "--long", "--always"], stderr=subprocess.PIPE
    +            )
    +            version = std_out.strip().decode("ascii")
    +            if "cgse" not in version.lower() and "ts" not in version.lower():
    +                version = None
    +        except subprocess.CalledProcessError as exc:
    +            version = None
    +
    +    return version
    +
    +
    +
    +def get_version_from_settings(group_name: str, location: Path = None) +
    +
    +

    Reads the VERSION field from the settings.yaml file. This function first tries to load the proper Settings +and Group and if that fails uses the raw method.

    +

    Args

    +
    +
    group_name
    +
    major group name that contains the VERSION field, i.e. Common-EGSE or PLATO_TEST_SCRIPTS.
    +
    location
    +
    the location of the settings.yaml file or None in which case the location of this file is used.
    +
    +

    Raises

    +

    A RuntimeError when the group_name is incorrect and unknown or the VERSION field is not found.

    +

    Returns

    +

    The version from the settings.yaml file as a string.

    +
    + +Expand source code + +
    def get_version_from_settings(group_name: str, location: Path = None):
    +    """
    +    Reads the VERSION field from the `settings.yaml` file. This function first tries to load the proper Settings
    +    and Group and if that fails uses the raw method.
    +
    +    Args:
    +        group_name: major group name that contains the VERSION field, i.e. Common-EGSE or PLATO_TEST_SCRIPTS.
    +        location: the location of the `settings.yaml` file or None in which case the location of this file is used.
    +
    +    Raises:
    +        A RuntimeError when the group_name is incorrect and unknown or the VERSION field is not found.
    +
    +    Returns:
    +        The version from the `settings.yaml` file as a string.
    +    """
    +    from egse.settings import Settings, SettingsError
    +
    +    try:
    +        settings = Settings.load(group_name, location=location)
    +        version = settings.VERSION
    +    except (ModuleNotFoundError, SettingsError):
    +        version = get_version_from_settings_file_raw(group_name, location=location)
    +
    +    return version
    +
    +
    +
    +def get_version_installed(package_name: str) ‑> str +
    +
    +

    Returns the version that is installed, i.e. read from the metadata in the import lib.

    +

    Args

    +
    +
    package_name
    +
    the name of the installed package, e.g. cgse or cgse-ts
    +
    +

    Returns

    +

    The version of the installed repo.

    +
    + +Expand source code + +
    def get_version_installed(package_name: str) -> str:
    +    """
    +    Returns the version that is installed, i.e. read from the metadata in the import lib.
    +
    +    Args:
    +        package_name: the name of the installed package, e.g. cgse or cgse-ts
    +
    +    Returns:
    +        The version of the installed repo.
    +    """
    +    from egse.system import chdir
    +
    +    with chdir(Path(__file__).parent):
    +        from importlib.metadata import version, PackageNotFoundError
    +
    +        try:
    +            version = version(package_name)
    +        except PackageNotFoundError as exc:
    +            version = None
    +
    +    return version
    +
    +
    +
    @@ -106,6 +383,13 @@

    Index

  • egse
  • +
  • Functions

    + +
  • diff --git a/docs/api/egse/visitedpositions.html b/docs/api/egse/visitedpositions.html index 0dace5e..c358f2e 100644 --- a/docs/api/egse/visitedpositions.html +++ b/docs/api/egse/visitedpositions.html @@ -833,7 +833,7 @@

    Args

    class VisitedPositionsUIView
    -

    QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    +

    QMainWindow(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

    Open a window and initialise the GUI.

    The focal plane is shown, together with a blue circle indicating the size of the field-of-view, and a red dots indicating the positions that were visited so far. diff --git a/docs/api/egse/zmq/index.html b/docs/api/egse/zmq/index.html index 9d1aa79..d91af9c 100644 --- a/docs/api/egse/zmq/index.html +++ b/docs/api/egse/zmq/index.html @@ -45,6 +45,8 @@

    Module egse.zmq

    SYNC_TIMECODE = 0x80 SYNC_HK_PACKET = 0x81 SYNC_DATA_PACKET = 0x82 + SYNC_ERROR_FLAGS = 0x85 + SYNC_HK_DATA = 0x86 N_FEE_REGISTER_MAP = 0x83 NUM_CYCLES = 0x84 @@ -97,6 +99,8 @@

    Classes

    SYNC_TIMECODE = 0x80 SYNC_HK_PACKET = 0x81 SYNC_DATA_PACKET = 0x82 + SYNC_ERROR_FLAGS = 0x85 + SYNC_HK_DATA = 0x86 N_FEE_REGISTER_MAP = 0x83 NUM_CYCLES = 0x84 @@ -133,6 +137,14 @@

    Class variables

    +
    var SYNC_ERROR_FLAGS
    +
    +
    +
    +
    var SYNC_HK_DATA
    +
    +
    +
    var SYNC_HK_PACKET
    @@ -172,6 +184,8 @@

    NUM_CYCLES
  • N_FEE_REGISTER_MAP
  • SYNC_DATA_PACKET
  • +
  • SYNC_ERROR_FLAGS
  • +
  • SYNC_HK_DATA
  • SYNC_HK_PACKET
  • SYNC_TIMECODE
  • diff --git a/docs/api/egse/zmq/spw.html b/docs/api/egse/zmq/spw.html index 2019f88..4bed77e 100644 --- a/docs/api/egse/zmq/spw.html +++ b/docs/api/egse/zmq/spw.html @@ -31,16 +31,12 @@

    Module egse.zmq.spw

    import zmq -from egse.spw import CRITICAL_AREA_END -from egse.spw import CRITICAL_AREA_START +import egse.rmap +import egse.spw from egse.spw import ReadRequestReply from egse.spw import SpaceWireInterface from egse.spw import SpaceWirePacket from egse.spw import WriteRequestReply -from egse.spw import create_rmap_read_request_packet -from egse.spw import create_rmap_unverified_write_packet -from egse.spw import create_rmap_verified_write_packet -from egse.spw import update_transaction_identifier LOGGER = logging.getLogger(__name__) @@ -56,7 +52,7 @@

    Module egse.zmq.spw

    self._transaction_id = 0 def connect(self): - LOGGER.log(5, f"Called connect() method of {self.__class__.__name__}.") + LOGGER.debug(f"Called {self.__class__.__name__}.connect().") self._zcontext = zmq.Context.instance() if "*" in self._endpoint: self._zsock = self._zcontext.socket(zmq.DEALER) @@ -75,15 +71,16 @@

    Module egse.zmq.spw

    self._zsock.close(linger=0) def configure(self): - LOGGER.flash_flood(f"Called configure() method of {self.__class__.__name__}.") + LOGGER.debug(f"Called configure() method of {self.__class__.__name__}.") def flush(self): - LOGGER.flash_flood(f"Called flush() method of {self.__class__.__name__}.") + LOGGER.debug(f"Called flush() method of {self.__class__.__name__}.") - def send_timecode(self, timecode: int): - LOGGER.flash_flood(f"Called send_timecode() method of {self.__class__.__name__}.") + def send_timecode(self, timecode: int) -> int: + LOGGER.debug(f"Called {self.__class__.__name__}.send_timecode({timecode}).") packet = bytes([0x91, timecode]) self._zsock.send_multipart([self._identity, pickle.dumps(packet)]) + return 0 def read_packet(self, timeout: int = None): @@ -98,29 +95,30 @@

    Module egse.zmq.spw

    return None, None if self._zsock not in socks: - # This happens most of the time, no RMAP commands, therefore commented out + # This happens most of the time, no RMAP commands, therefore commented out the log msg # LOGGER.warning(f"No message received during {timeout=} seconds.") return None, None - LOGGER.flash_flood(f"Called read_packet() method of {self.__class__.__name__}.") + # LOGGER.debug(f"Called read_packet() method of {self.__class__.__name__}.") identity, pickle_string = self._zsock.recv_multipart() if not pickle_string: return None, None - LOGGER.flash_flood(f"identity = {identity}, pickle_string = {type(pickle_string)}") + # LOGGER.debug(f"identity = {identity}, pickle_string = {type(pickle_string)}") response = pickle.loads(pickle_string) - LOGGER.flash_flood(f"response = {response[:20]}") + # LOGGER.debug(f"response = {response[:20]}") return None, response def write_packet(self, packet: bytes): - LOGGER.flash_flood(f"Called write_packet() method of {self.__class__.__name__}.") + LOGGER.debug(f"Called {self.__class__.__name__}.write_packet({packet[:10]=}).") self._zsock.send_multipart([self._identity, pickle.dumps(packet)]) def read_register(self, address: int, length: int = 4, strict: bool = True) -> bytes: - LOGGER.flash_flood(f"Called read_register() method of {self.__class__.__name__}.") + LOGGER.debug(f"Called {self.__class__.__name__}.read_register(0x{address:0x}, {length=}).") - self._transaction_id = update_transaction_identifier(self._transaction_id) - buffer = create_rmap_read_request_packet(address, length, self._transaction_id, strict=strict) + self._transaction_id = egse.spw.update_transaction_identifier(self._transaction_id) + + buffer = egse.rmap.create_rmap_read_request_packet(address, length, self._transaction_id, strict=strict) self.write_packet(buffer) _, data = self.read_packet() @@ -141,13 +139,14 @@

    Module egse.zmq.spw

    return data def write_register(self, address: int, data: bytes): - LOGGER.log(10, f"Called write_register() method of {self.__class__.__name__}.") + LOGGER.debug(f"Called {self.__class__.__name__}.write_register(0x{address:0x}, {data}) method.") + + self._transaction_id = egse.spw.update_transaction_identifier(self._transaction_id) - self._transaction_id = update_transaction_identifier(self._transaction_id) - if CRITICAL_AREA_START <= address <= CRITICAL_AREA_END: - buffer = create_rmap_verified_write_packet(address, data, self._transaction_id) + if egse.rmap.CRITICAL_AREA_START <= address <= egse.rmap.CRITICAL_AREA_END: + buffer = egse.rmap.create_rmap_verified_write_packet(address, data, self._transaction_id) else: - buffer = create_rmap_unverified_write_packet( + buffer = egse.rmap.create_rmap_unverified_write_packet( address, data, len(data), self._transaction_id) self.write_packet(buffer) @@ -161,7 +160,32 @@

    Module egse.zmq.spw

    LOGGER.error(f"Expected RMAP write request reply packet, got {packet.__class__.__name__}") status = -1 - return status
    + return status + + def read_memory_map(self, address: int, length: int = 4) -> bytes: + LOGGER.debug(f"Called read_memory_map({address}, {length}) method of {self.__class__.__name__}.") + + self._transaction_id = egse.spw.update_transaction_identifier(self._transaction_id) + + buffer = egse.rmap.create_rmap_read_request_packet(address, length, self._transaction_id, strict=True) + + self.write_packet(buffer) + _, data = self.read_packet() + + packet = SpaceWirePacket.create_packet(data) + + if isinstance(packet, ReadRequestReply): + data = packet.data + + if len(data) != length: + LOGGER.warning( + f"Expected data to be {length} bytes, but received {len(data)} bytes. {data=}") + else: + LOGGER.error( + f"Expected RMAP read request reply packet, got {packet.__class__.__name__}") + data = bytes() + + return data
    @@ -194,7 +218,7 @@

    Classes

    self._transaction_id = 0 def connect(self): - LOGGER.log(5, f"Called connect() method of {self.__class__.__name__}.") + LOGGER.debug(f"Called {self.__class__.__name__}.connect().") self._zcontext = zmq.Context.instance() if "*" in self._endpoint: self._zsock = self._zcontext.socket(zmq.DEALER) @@ -213,15 +237,16 @@

    Classes

    self._zsock.close(linger=0) def configure(self): - LOGGER.flash_flood(f"Called configure() method of {self.__class__.__name__}.") + LOGGER.debug(f"Called configure() method of {self.__class__.__name__}.") def flush(self): - LOGGER.flash_flood(f"Called flush() method of {self.__class__.__name__}.") + LOGGER.debug(f"Called flush() method of {self.__class__.__name__}.") - def send_timecode(self, timecode: int): - LOGGER.flash_flood(f"Called send_timecode() method of {self.__class__.__name__}.") + def send_timecode(self, timecode: int) -> int: + LOGGER.debug(f"Called {self.__class__.__name__}.send_timecode({timecode}).") packet = bytes([0x91, timecode]) self._zsock.send_multipart([self._identity, pickle.dumps(packet)]) + return 0 def read_packet(self, timeout: int = None): @@ -236,29 +261,30 @@

    Classes

    return None, None if self._zsock not in socks: - # This happens most of the time, no RMAP commands, therefore commented out + # This happens most of the time, no RMAP commands, therefore commented out the log msg # LOGGER.warning(f"No message received during {timeout=} seconds.") return None, None - LOGGER.flash_flood(f"Called read_packet() method of {self.__class__.__name__}.") + # LOGGER.debug(f"Called read_packet() method of {self.__class__.__name__}.") identity, pickle_string = self._zsock.recv_multipart() if not pickle_string: return None, None - LOGGER.flash_flood(f"identity = {identity}, pickle_string = {type(pickle_string)}") + # LOGGER.debug(f"identity = {identity}, pickle_string = {type(pickle_string)}") response = pickle.loads(pickle_string) - LOGGER.flash_flood(f"response = {response[:20]}") + # LOGGER.debug(f"response = {response[:20]}") return None, response def write_packet(self, packet: bytes): - LOGGER.flash_flood(f"Called write_packet() method of {self.__class__.__name__}.") + LOGGER.debug(f"Called {self.__class__.__name__}.write_packet({packet[:10]=}).") self._zsock.send_multipart([self._identity, pickle.dumps(packet)]) def read_register(self, address: int, length: int = 4, strict: bool = True) -> bytes: - LOGGER.flash_flood(f"Called read_register() method of {self.__class__.__name__}.") + LOGGER.debug(f"Called {self.__class__.__name__}.read_register(0x{address:0x}, {length=}).") + + self._transaction_id = egse.spw.update_transaction_identifier(self._transaction_id) - self._transaction_id = update_transaction_identifier(self._transaction_id) - buffer = create_rmap_read_request_packet(address, length, self._transaction_id, strict=strict) + buffer = egse.rmap.create_rmap_read_request_packet(address, length, self._transaction_id, strict=strict) self.write_packet(buffer) _, data = self.read_packet() @@ -279,13 +305,14 @@

    Classes

    return data def write_register(self, address: int, data: bytes): - LOGGER.log(10, f"Called write_register() method of {self.__class__.__name__}.") + LOGGER.debug(f"Called {self.__class__.__name__}.write_register(0x{address:0x}, {data}) method.") + + self._transaction_id = egse.spw.update_transaction_identifier(self._transaction_id) - self._transaction_id = update_transaction_identifier(self._transaction_id) - if CRITICAL_AREA_START <= address <= CRITICAL_AREA_END: - buffer = create_rmap_verified_write_packet(address, data, self._transaction_id) + if egse.rmap.CRITICAL_AREA_START <= address <= egse.rmap.CRITICAL_AREA_END: + buffer = egse.rmap.create_rmap_verified_write_packet(address, data, self._transaction_id) else: - buffer = create_rmap_unverified_write_packet( + buffer = egse.rmap.create_rmap_unverified_write_packet( address, data, len(data), self._transaction_id) self.write_packet(buffer) @@ -299,7 +326,32 @@

    Classes

    LOGGER.error(f"Expected RMAP write request reply packet, got {packet.__class__.__name__}") status = -1 - return status + return status + + def read_memory_map(self, address: int, length: int = 4) -> bytes: + LOGGER.debug(f"Called read_memory_map({address}, {length}) method of {self.__class__.__name__}.") + + self._transaction_id = egse.spw.update_transaction_identifier(self._transaction_id) + + buffer = egse.rmap.create_rmap_read_request_packet(address, length, self._transaction_id, strict=True) + + self.write_packet(buffer) + _, data = self.read_packet() + + packet = SpaceWirePacket.create_packet(data) + + if isinstance(packet, ReadRequestReply): + data = packet.data + + if len(data) != length: + LOGGER.warning( + f"Expected data to be {length} bytes, but received {len(data)} bytes. {data=}") + else: + LOGGER.error( + f"Expected RMAP read request reply packet, got {packet.__class__.__name__}") + data = bytes() + + return data

    Ancestors

      @@ -317,7 +369,7 @@

      Methods

      Expand source code
      def configure(self):
      -    LOGGER.flash_flood(f"Called configure() method of {self.__class__.__name__}.")
      + LOGGER.debug(f"Called configure() method of {self.__class__.__name__}.")
      @@ -330,7 +382,7 @@

      Methods

      Expand source code
      def connect(self):
      -    LOGGER.log(5, f"Called connect() method of {self.__class__.__name__}.")
      +    LOGGER.debug(f"Called {self.__class__.__name__}.connect().")
           self._zcontext = zmq.Context.instance()
           if "*" in self._endpoint:
               self._zsock = self._zcontext.socket(zmq.DEALER)
      @@ -369,22 +421,7 @@ 

      Methods

      Expand source code
      def flush(self):
      -    LOGGER.flash_flood(f"Called flush() method of {self.__class__.__name__}.")
      - - -
      -def send_timecode(self, timecode: int) -
      -
      -
      -
      - -Expand source code - -
      def send_timecode(self, timecode: int):
      -    LOGGER.flash_flood(f"Called send_timecode() method of {self.__class__.__name__}.")
      -    packet = bytes([0x91, timecode])
      -    self._zsock.send_multipart([self._identity, pickle.dumps(packet)])
      + LOGGER.debug(f"Called flush() method of {self.__class__.__name__}.")
      @@ -395,6 +432,7 @@

      Inherited members

    • read_memory_map
    • read_packet
    • read_register
    • +
    • send_timecode
    • write_packet
    • write_register
    @@ -424,7 +462,6 @@

    connect
  • disconnect
  • flush
  • -
  • send_timecode
  • diff --git a/docs/asciidocs/commanding-manual.html b/docs/asciidocs/commanding-manual.html index 80083be..71aaf3a 100644 --- a/docs/asciidocs/commanding-manual.html +++ b/docs/asciidocs/commanding-manual.html @@ -4,7 +4,7 @@ - + Ground Tests Commanding Manual @@ -85,10 +85,10 @@ ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} ul,ol{margin-left:1.5em} ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0} -ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} -ul.square{list-style-type:square} ul.circle{list-style-type:circle} ul.disc{list-style-type:disc} +ul.square{list-style-type:square} +ul.circle ul:not([class]),ul.disc ul:not([class]),ul.square ul:not([class]){list-style:inherit} ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} dl dt{margin-bottom:.3125em;font-weight:bold} dl dd{margin-bottom:1.25em} @@ -209,13 +209,10 @@ .admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere} .admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} .exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px} -.exampleblock>.content>:first-child{margin-top:0} -.exampleblock>.content>:last-child{margin-bottom:0} .sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px} -.sidebarblock>:first-child{margin-top:0} -.sidebarblock>:last-child{margin-bottom:0} .sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} -.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} +.exampleblock>.content>:first-child,.sidebarblock>.content>:first-child{margin-top:0} +.exampleblock>.content>:last-child,.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} .literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em} @media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}} @media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}} @@ -394,7 +391,7 @@ dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility} h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em} p strong,td.content strong,div.footnote strong{letter-spacing:-.005em} -p,blockquote,dt,td.content,span.alt,summary{font-size:1.0625rem} +p,blockquote,dt,td.content,td.hdlist1,span.alt,summary{font-size:1.0625rem} p{margin-bottom:1.25rem} .sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} .exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc} @@ -438,73 +435,113 @@ @@ -514,8 +551,8 @@

    Ground Tests Commanding Manual

    KU Leuven PLATO Team
    -version 0.10, -29/06/2023 +version 0.11, +12/02/2024
    Table of Contents
    @@ -692,6 +729,16 @@

    Changelog

    +
    12/02/2024 — 0.11
    +
    +
    +
      +
    • +

      Added a warning that loading/submitting a Setup doesn’t propagate this new Setup to all components in the system, see Chapter 6

      +
    • +
    +
    +
    29/06/2023 — 0.10
    @@ -920,24 +967,24 @@

    Conventions used in this Book

    -
    -
    +
    +
    +
      -
    • +
    • FM

    • -
    • +
    • EM

    -
    -
    +

    In this TAB we present FM specific information.

    -
    +

    In this TAB we present EM specific information.

    @@ -1176,6 +1223,10 @@

    Acronyms

    Camera

    +

    CCD

    +

    Charged-Coupled Device

    + +

    CGSE

    Common-EGSE

    @@ -1184,6 +1235,10 @@

    Acronyms

    Centre Spatial de Liège

    +

    CSV

    +

    Comma-Separated Values

    + +

    COT

    Commercial off-the-shelf

    @@ -1192,6 +1247,10 @@

    Acronyms

    Charge Transfer Inefficiency

    +

    CTS

    +

    Consent to Ship

    + +

    DPU

    Data Processing Unit

    @@ -1208,10 +1267,18 @@

    Acronyms

    End Of Life

    +

    FAQ

    +

    Frequently Asked Questions

    + +

    FEE

    Front End Electronics

    +

    FITS

    +

    Flexible Image Transport System

    + +

    FPA

    Focal Plane Assembly/Array

    @@ -1220,6 +1287,14 @@

    Acronyms

    Ground Support Equipment

    +

    GUI

    +

    Graphical User Interface

    + + +

    HDF5

    +

    Hierarchical Data Format version 5 (File format)

    + +

    HK

    Housekeeping

    @@ -1252,6 +1327,10 @@

    Acronyms

    Non-Conformance Review Board

    +

    OBSID

    +

    Observation Identifier

    + +

    OGSE

    Optical Ground Support Equipment

    @@ -1260,10 +1339,18 @@

    Acronyms

    Operating System

    +

    PDF

    +

    Portable Document Format

    + +

    PID

    Process Identifier

    +

    PLATO

    +

    PLAnetary Transits and Oscillations of stars

    + +

    PPID

    Parent Process Identifier

    @@ -1292,6 +1379,10 @@

    Acronyms

    SpaceWire

    +

    SQL

    +

    Structured Query Language

    + +

    SRON

    Stichting Ruimte-Onderzoek Nederland

    @@ -1340,13 +1431,25 @@

    Acronyms

    Test Scripts

    +

    TUI

    +

    Text-based User Interface

    + +

    TV

    Thermal Vacuum

    +

    UM

    +

    User Manual

    + +

    USB

    Universal Serial Bus

    + +

    YAML

    +

    YAML Ain’t Markup Language

    +
    @@ -3370,6 +3473,18 @@

    6.1. Example Setup file

    │ ├── 2: Removed TCS block
    +
    +

    You can check the version of the Setup with the following command:

    +
    +
    +
    +
    >>> print(setup.get_id())
    +00068
    +
    +
    +
    +

    The number printed on your system will be different.

    +

    6.2. Available Setups

    @@ -3512,6 +3627,42 @@

    6.3. Loading a Setup

    >>> setup = load_setup(7)
    +
    +

    The above command will load the Setup in the configuration manager and also in the Python console you used to execute the load_setup() command.

    +
    +
    + + + + + +
    +
    Warning
    +
    +Loading or submitting a Setup will have an effect on the configuration manager. The Setup is however NOT automatically propagated to other components in the system like the device drivers or the synoptics manager. Therefore, always check if you have the right Setup available, especially in the QtConsole. +
    +
    +
    +

    You can check which Setup is loaded in the configuration manager with

    +
    +
    +
    +
    $ cm_cs status
    +Configuration manager:
    +  Status: active
    +  Site ID: CSL2
    +  No observation running
    +  Setup loaded: 00068
    +  Hostname: 192.168.0.163
    +  Monitoring port: 6001
    +  Commanding port: 6000
    +  Service port: 6002
    +  Listeners: Storage CS
    +
    +
    +
    +

    Also the process manager (pm_ui) and the Operator GUI (e.g. csl_ui) indicate which Setup is loaded, but keep in mind that this is only true for that particular process and is not necessarily propagated to all processes running in this distributed environment.

    +

    6.4. Inspecting, accessing, and modifying a Setup

    @@ -6241,19 +6392,19 @@

    12.4.1. CCD and pixel references

    -
    -
    +
    +
    +
      -
    • +
    • FM

    • -
    • +
    • EM

    -
    -
    +
    ccd numbering coordinates fm @@ -6261,7 +6412,7 @@

    12.4.1. CCD and pixel references

    Figure 24. CCD numbering and pixel coordinates for the FM camera on every CCD (CCD_PIXn reference frames). The thicker black border lines represent the readout registers.
    -
    +
    ccd numbering coordinates em @@ -8067,55 +8218,136 @@

    17.E.3. PVS

    diff --git a/docs/asciidocs/developer-manual.html b/docs/asciidocs/developer-manual.html index 3170cfe..6b5fce1 100644 --- a/docs/asciidocs/developer-manual.html +++ b/docs/asciidocs/developer-manual.html @@ -4,7 +4,7 @@ - + Common–EGSE : Developer Manual @@ -85,10 +85,10 @@ ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} ul,ol{margin-left:1.5em} ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0} -ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} -ul.square{list-style-type:square} ul.circle{list-style-type:circle} ul.disc{list-style-type:disc} +ul.square{list-style-type:square} +ul.circle ul:not([class]),ul.disc ul:not([class]),ul.square ul:not([class]){list-style:inherit} ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} dl dt{margin-bottom:.3125em;font-weight:bold} dl dd{margin-bottom:1.25em} @@ -209,13 +209,10 @@ .admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere} .admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} .exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px} -.exampleblock>.content>:first-child{margin-top:0} -.exampleblock>.content>:last-child{margin-bottom:0} .sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px} -.sidebarblock>:first-child{margin-top:0} -.sidebarblock>:last-child{margin-bottom:0} .sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} -.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} +.exampleblock>.content>:first-child,.sidebarblock>.content>:first-child{margin-top:0} +.exampleblock>.content>:last-child,.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} .literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em} @media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}} @media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}} @@ -394,7 +391,7 @@ dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility} h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em} p strong,td.content strong,div.footnote strong{letter-spacing:-.005em} -p,blockquote,dt,td.content,span.alt,summary{font-size:1.0625rem} +p,blockquote,dt,td.content,td.hdlist1,span.alt,summary{font-size:1.0625rem} p{margin-bottom:1.25rem} .sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} .exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc} @@ -439,73 +436,113 @@ @@ -515,8 +552,8 @@

    Common–EGSE : Developer Manual

    Rik Huygen, Sara Regibo
    -version 1.7, -19/06/2023 +version 1.8, +12/02/2024
    Table of Contents
    @@ -596,130 +633,153 @@

    Common–EGSE : Developer Manual

  • 6.6. Dynamic Commanding
  • -
  • 7. The Setup +
  • 7. Notifications for Control Servers + +
  • +
  • 8. The Setup
  • -
  • 8. The Settings Class
  • -
  • 9. The GlobalState +
  • 9. The Settings Class
  • +
  • 10. The GlobalState
  • -
  • 10. Data Strategy
  • -
  • 11. Date, Time and Timestamps +
  • 11. Data Strategy
  • +
  • 12. Date, Time and Timestamps
  • Part III — Device Commanding
  • Part IV — Monitoring
  • Part V — Test Scripts
  • Part VI — Unit Testing and beyond
  • Part VII — Miscellaneous @@ -742,12 +802,25 @@

    Changelog

    +
    12/02/2024 — v1.8
    +
    +
    +
      +
    • +

      Started a chapter on SpaceWire communication, see Chapter 15.

      +
    • +
    • +

      Added a chapter on the notification system for control servers, see Chapter 7.

      +
    • +
    +
    +
    18/06/2023 — v1.7
    • -

      Added a section on setting up a development environment to contribute to the CGSE and/or TS, see Chapter 22.

      +

      Added a section on setting up a development environment to contribute to the CGSE and/or TS, see Chapter 24.

    • added a backlink to the CGSE Documentation web site for your convenience. It’s at the top of the HTML page.

      @@ -760,13 +833,13 @@

      Changelog

      • -

        Added explanation about data distribution and monitoring message queues used by the DPU Processor to publish information and data, see Chapter 13.

        +

        Added explanation about data distribution and monitoring message queues used by the DPU Processor to publish information and data, see Chapter 14.

      • Added some more meat into the description of the arguments for the @dynamic_command decorator, see Section 6.6.2.

      • -

        Added an explanation of the communication with and commanding of the N-FEE in the inner loop of the DPUProcessor, see Section 13.1.1.

        +

        Added an explanation of the communication with and commanding of the N-FEE in the inner loop of the DPUProcessor, see Section 14.1.1.

      @@ -886,24 +959,24 @@

      Conventions used in this Book

    -
    -
    +
    +
    +
      -
    • +
    • FM

    • -
    • +
    • EM

    -
    -
    +

    In this TAB we present FM specific information.

    -
    +

    In this TAB we present EM specific information.

    @@ -1205,6 +1278,10 @@

    Acronyms

    Camera

    +

    CCD

    +

    Charged-Coupled Device

    + +

    CGSE

    Common-EGSE

    @@ -1213,6 +1290,10 @@

    Acronyms

    Centre Spatial de Liège

    +

    CSV

    +

    Comma-Separated Values

    + +

    COT

    Commercial off-the-shelf

    @@ -1221,6 +1302,10 @@

    Acronyms

    Charge Transfer Inefficiency

    +

    CTS

    +

    Consent to Ship

    + +

    DPU

    Data Processing Unit

    @@ -1237,10 +1322,18 @@

    Acronyms

    End Of Life

    +

    FAQ

    +

    Frequently Asked Questions

    + +

    FEE

    Front End Electronics

    +

    FITS

    +

    Flexible Image Transport System

    + +

    FPA

    Focal Plane Assembly/Array

    @@ -1249,6 +1342,14 @@

    Acronyms

    Ground Support Equipment

    +

    GUI

    +

    Graphical User Interface

    + + +

    HDF5

    +

    Hierarchical Data Format version 5 (File format)

    + +

    HK

    Housekeeping

    @@ -1281,6 +1382,10 @@

    Acronyms

    Non-Conformance Review Board

    +

    OBSID

    +

    Observation Identifier

    + +

    OGSE

    Optical Ground Support Equipment

    @@ -1289,10 +1394,18 @@

    Acronyms

    Operating System

    +

    PDF

    +

    Portable Document Format

    + +

    PID

    Process Identifier

    +

    PLATO

    +

    PLAnetary Transits and Oscillations of stars

    + +

    PPID

    Parent Process Identifier

    @@ -1321,6 +1434,10 @@

    Acronyms

    SpaceWire

    +

    SQL

    +

    Structured Query Language

    + +

    SRON

    Stichting Ruimte-Onderzoek Nederland

    @@ -1369,13 +1486,25 @@

    Acronyms

    Test Scripts

    +

    TUI

    +

    Text-based User Interface

    + +

    TV

    Thermal Vacuum

    +

    UM

    +

    User Manual

    + +

    USB

    Universal Serial Bus

    + +

    YAML

    +

    YAML Ain’t Markup Language

    +
    @@ -3202,7 +3331,371 @@

    6.6.3. Update your code to use the @dynamic_co

    -

    7. The Setup

    +

    7. Notifications for Control Servers

    +
    +
    +

    This section explains how notifications for control servers are designed and implemented with an Event Listeners pattern. We will talk about events between two processes, two control servers. One control server will send out notifications for an event and is therefore called the notifying control server or the Producer, the other control server is listening for events and is therefore called the listening control server, or the Consumer.

    +
    +
    +

    The basic requirements we have for the event notification are:

    +
    +
    +
      +
    • +

      Any control server shall be able to accept a registration for a notification of an event.

      +
    • +
    • +

      It shall be possible to unregister from receiving events.

      +
    • +
    • +

      The event generator (producer) shall not wait for a reply from the listener.

      +
    • +
    • +

      The Listener shall handle events as soon as possible.

      +
    • +
    +
    +
    +

    The following terms will be used throughout this chapter. In order to try to avoid confusion, we will briefly explain those in the context of the CGSE.

    +
    +
    + + + + + + + + + + + + + +
    +Event + +

    The event is the object that carries the information about a change that needs to be reported to the listeners.

    +
    +Listener + +

    The process that registers to a control server for an event notification is called a listener or consumer, i.e. it listens for one or more specific events that are sent out by another control server process.

    +
    +Event Generator + +

    The control server process that sends out notifications to its listeners. Sometimes its called a producer. This control server holds registrations for all listeners that need to be notified of an event.

    +
    +
    +
    +

    In the case of the CGSE, a control server can act both as the event generator and the listener. Not that the control server will send events to itself, but it will send events to other control servers. Therefore, the implementation will include both sides of the event listener pattern, i.e. producer and consumer. The pattern will be implemented with ZeroMQ messaging. This is different from the usual implementation of this pattern where a producer object calls a method on a consumer object. In our case, the event generator process, will send a ZeroMQ message, containing the event object, to the listener process.

    +
    +
    +

    7.1. The Event

    +
    +

    An Event is an object that contains all information needed by the listeners in order to process the event notification.

    +
    +
    +

    An Event is defined by an event identifier and an event type. Currently, the only event identified is the New Setup event notification with event_id: EVENT_ID.SETUP and event_type: "new_setup". An Event also needs a context which contains information that is needed by the listener in order to handle the event. The context contains event specific information, e.g. in the case of a new Setup, the context also contains the setup identifier (setup_id).

    +
    +
    +

    The control server that wants to be notified of an event needs to implement the EventInterface in the Proxy and in the Controller. This interface currently contains only one method, ie. handle_event(event) which takes the event object as its only argument.

    +
    +
    +
    +

    7.2. The Listener

    +
    +

    Listeners are registered and unregistered through the services interface of the control server. That means the developer of a control server that needs to listen to events doesn’t need to implement the registration functions, only the handling of the events must be implemented.

    +
    +
    +

    When a listener is registered, a name and a proxy shall be provided. The name is used as an identifier for the listener, the proxy is the Proxy class for the listening process. The proxy will be used by the producer to notify the listener/consumer.

    +
    +
    +

    When registering, another proxy shall be provided, which is the producers proxy, i.e. the proxy for the control server to which the listener wants to register. So, for example, if the storage manager wants to listen to events on new Setups from the configuration manager, that proxy shall be a ConfigurationManagerProxy. The following code snippet should make this clear:

    +
    +
    +
    +
    self.register_as_listener(
    +    proxy=ConfigurationManagerProxy,
    +    listener={'name': 'Storage CS', 'proxy': StorageProxy, 'event_id': EVENT_ID.SETUP}
    +)
    +
    +
    +
    +

    Registrations are issued from the ControlServer and handled by the ServiceProtocol. The registration is executed in a separate thread and retried 5 times with a 10s wait time in between.

    +
    +
    +

    Event notifications are handled by executing the handle_event() method on the consumer Proxy. The handle_event command is defined by the EventInterface which is a base class for the Controller and the Proxy. Both the Controller class and the Proxy class need to inherit from the EventInterface.

    +
    +
    +
    +
    class EventInterface:
    +
    +    @dynamic_interface
    +    def handle_event(self, event: Event):
    +        ...
    +
    +
    +
    +

    In this case, we will also need to provide an entry in the command YAML file:

    +
    +
    +
    +
    handle_event:
    +    description:   Notification of an event
    +    cmd:           '{event}'
    +
    +
    +
    +
    +

    7.3. The Event Generator

    +
    +

    The control server that generates the events keeps a list of registered consumers/listeners. The registration of these listeners is handled by the service protocol. Event notifications are sent out from the ControlServer class, but they usually occur in the Controller class. The Controller therefore shall keep a reference to its control server. When the event is detected, the Controller will use the notify_listeners() method of the ControlServer. This latter method expects an event identifier and the event context. The example code snippet below sends out a new setup loaded notification to all registered listeners.

    +
    +
    +
    +
    self._control_server.notify_listeners(
    +    EVENT_ID.SETUP,
    +    {'event_type': 'new_setup', 'setup_id': setup_id}
    +)
    +
    +
    +
    +

    The notify_listeners() method will execute the actual notification in a separate thread in order not to block the commanding event loop.

    +
    +
    +
    +

    7.4. The Service Protocol

    +
    +

    The registration, management, and notification of listeners in a control server is handled by the service protocol. That means all control servers automatically have this functionality available. There is only one method that needs to be implemented in the listening control server and that is the handling of the event. That is defined in the EventInterface which the Proxy and the Controller of the listening control server shall inherit and implement.

    +
    +
    +

    New commands in Service:

    +
    +
    +
      +
    • +

      add_listener() – add a listener to the registration

      +
    • +
    • +

      remove_listener() – remove a listener from the registration

      +
    • +
    • +

      get_listener_names() – returns the names of the control servers that are registered as listener

      +
    • +
    +
    +
    +

    New commands in ControlServer:

    +
    +
    +
      +
    • +

      register_as_listener() — register this control server to a producer

      +
    • +
    • +

      unregister_as_listener() — unregisters this control server

      +
    • +
    • +

      notify_listeners() — send an event notification to all registered listeners

      +
    • +
    • +

      get_listener_names() — return a list of names of registered listeners

      +
    • +
    +
    +
    +
    +

    7.5. Scheduled Tasks

    +
    +

    The scheduled tasks are functions known in the control server sub-class which will be executed outside the context of commanding and monitoring. The reason we need these scheduled tasks is to handle actions that either take too long during the execution of a command, or when a command request is sent back to the same control server that the original command came from, e.g. request the current Setup. In that latter case we will get a deadlock because both control servers will be waiting for each others reply.

    +
    +
    +

    Scheduled tasks are executed from the ControlServers event loop (serve()) after the current command has been finished and the status and housekeeping has been published. Keep in mind that –at this point– the tasks should not take too much time or shall either run in a thread or run asynchronously.

    +
    +
    +

    Tasks can also be scheduled after a certain time, e.g. after 10s (approximately), or can be scheduled when a certain condition is true, e.g., when a service comes on-line. The after and when can be combined.

    +
    +
    +
    +

    7.6. The Notification Process

    +
    +

    This section explains what the natural flow is for notifications, starting at an event happening in a control server up to processing the event in the registered listener processes.

    +
    +
    +

    As an example we will use the event that a new Setup will be loaded in the configuration manager. This can happen when the configuration manager is requested to load a new Setup (load_setup()) or when a client submits a new Setup to the configuration manager (submit_setup()). The configuration manager will then send a notification of this event to its registered listeners. The listeners shall implement a command to accept such an event notification. By convention, this command is handle_event(event) (defined in the EventInterface). So, if you need to be notified about a new Setup, your control server needs to implement the handle_event command in its commanding interface. See further XXXXX

    +
    +
    +

    A control server that wants to listen to events needs to register first. This shall be done during startup, usually in the __init__() method of the listening control server. Registration requires a proxy for the notifying control server that will accept the registration, and a listener object (a dictionary) with information about the listener process.

    +
    +
    +
    +
    self.register_as_listener(
    +    proxy=ConfigurationManagerProxy,
    +    listener={'name': 'Storage CS', 'proxy': StorageProxy, 'event_id': EVENT_ID.SETUP}
    +)
    +
    +
    +
    +

    The notifying control server will accept the registration and add the listener to its list of registrations.

    +
    +
    +

    When an event occurs at the notifying control server, an event notification will be sent out to all the registered listeners. This is done by the notify_listeners() method of the ControlServer.

    +
    +
    +

    The listening control server will receive the notification, execute or schedule a task to act on the event, and acknowledge the proper reception of the notification. This is done in the handle_event() method that needs to be implemented by the listening control server in its Controller class. The handling of the event shall be as fast as possible because the notifying control server will be waiting for an acknowledgment. Therefore, there are currently two options: + 1. the task is executed in a thread, + 2. the task is scheduled and will be executed from the event loop in the control server (serve()).

    +
    +
    +
    +

    7.7. The Configuration Manager

    +
    +
    +
    NOTE
    +
    +

    This section should describe how notification is implemented in the configuration control server as a producer.

    +
    +
    +
    +
    +

    The controller now keeps track of the Setup that is currently active. This value changes when a different Setup is loaded (with load_setup()) or when a new Setup is submitted (with submit_setup()) to the configuration manager.

    +
    +
    +

    If you want to know which processes have registered to the configuration manager, the status info has been updated for this. The last line now shows the registered listeners:

    +
    +
    +
    +
    $ cm_cs status
    +Configuration manager:
    +  Status: active
    +  Site ID: CSL2
    +  No observation running
    +  Setup loaded: 00053
    +  Hostname: 192.168.68.76
    +  Monitoring port: 6001
    +  Commanding port: 6000
    +  Service port: 6002
    +  Listeners: Storage CS, Dummy CS
    +
    +
    +
    +
    +

    7.8. The Storage Manager

    +
    +
    +
    NOTE
    +
    +

    This section should describe how notification is implemented in the storage manager control server as a consumer.

    +
    +
    +
    +
    +

    The Storage Manager can now handle events about a new Setup loaded in the configurations manager and will register to the configuration manager during startup. The Storage Controller now keeps a record of the currently loaded Setup in its Controller.

    +
    +
    +

    Since a Setup can only be changed outside the context of an observation, the implementation is not too complicated. The controller has a new method load_setup(id) which is executed as a scheduled task after a notification is received. That task will fetch the Setup from the configuration manager (the reason why we needed scheduled tasks to avoid a deadlock at this point).

    +
    +
    +

    The status information from the Storage manager now also includes the ID of the loaded Setup. See second last line in the example output below.

    +
    +
    +
    +
    Storage Manager:
    +  Status: active
    +  Hostname: 172.20.10.3
    +  Monitoring port: 6101
    +  Commanding port: 6100
    +  Service port: 6102
    +  Storage location: /Users/rik/data/CSL2
    +  Loaded Setup: 00053
    +  Registrations: ['obsid', 'CM', 'PM', 'SYN-HK', 'SYN', 'DUMMY-HK']
    +
    +
    +
    +
    +

    7.9. The Dummy Control Server

    +
    +

    As an example…​ XXXXX

    +
    +
    +

    The Dummy module is updated to implement event notification and listener registration. The Dummy control server will register to the configuration manager at start up and will handle any event it receives by logging the event reception and execution. In the shutdown phase after_serve() the Dummy control server unregisters from the configuration control manager.

    +
    +
    +
    +

    7.10. Adding event handling to a control server

    +
    +

    This section will guide you through the process of implementing event handling into a control server. As an example we will update the Synoptic Manager CS to register to the configuration manager for new Setup events and handle those events in a scheduled task.

    +
    +
    +

    So, the summary of changes that we are going to implement is:

    +
    +
    +
      +
    1. +

      Add registration and de-registration to the Synoptic Manager CS

      +
      +
        +
      • +

        add registration to __init__() method

        +
      • +
      • +

        add de-registration to the after_server() method

        +
      • +
      +
      +
    2. +
    3. +

      Implement the EventInterface by means of the handle_event() method

      +
      +
        +
      • +

        implement a load_setup function for the controller class

        +
      • +
      • +

        schedule the task to load the Setup

        +
      • +
      +
      +
    4. +
    +
    +
    +

    The Synoptic Manager is started only after the configuration manager has started up and will therefore be initialised with the proper Setup. No need to add a one-time scheduled task for loading the Setup at startup.

    +
    +
    +

    7.10.1. Add registration and de-registration

    +
    +

    Files:

    +
    +
    +
      +
    • +

      egse.synoptics.syn_cs.py

      +
    • +
    +
    +
    +
    +

    7.10.2. Implement the event handling

    +
    +

    Files:

    +
    +
    +
      +
    • +

      egse.synoptics.__init__.py

      +
    • +
    +
    +
    +

    XXXXX: shall we also add an example of an event handling task that takes too long to process and can therefore not be a scheduled task unless it is running in a thread?

    +
    +
    +
    +
    +
    +
    +

    8. The Setup

    TODO:

    @@ -3243,7 +3736,7 @@

    7. The Setup

    This section will explain what the Setup contains, when and where it is used, how the different Setups are managed and how you can create a new Setup.

    -

    7.1. What is the Setup?

    +

    8.1. What is the Setup?

    @@ -3311,7 +3804,7 @@

    7.1. What is the Setup?

    -

    7.2. What goes into the Setup?

    +

    8.2. What goes into the Setup?

    The Setup will contain configuration information of your test equipment. This includes calibration and conversion information, identification, alignment information, etc. All items that can change from one setup to the next should be fully described in the Setup.

    @@ -3339,7 +3832,7 @@

    7.2. What goes into the Setup?

    -

    7.3. How to use the Setup

    +

    8.3. How to use the Setup

    As described above, the Setup is a special dictionary that contains all the configuration information of your test equipment. This information resides in several files maintained by the configuration control server. You can request a list of Setups that are available from the configuration manager with the list_setups() function. This is a convenience function that will print the list in your Python session.

    @@ -3369,7 +3862,7 @@

    7.3. How to use the Setup

    -

    7.4. How to create and manage Setups

    +

    8.4. How to create and manage Setups

    Whenever you make a change to an item in the Setup, or you add a configuration item, a new Setup will be created with a new unique id as soon as you submit the Setup to the configuration manager.

    @@ -3431,7 +3924,7 @@

    7.4. How to create and manage Setups

    -

    8. The Settings Class

    +

    9. The Settings Class

      @@ -3516,7 +4009,7 @@

      8. The Settings Class

    -

    9. The GlobalState

    +

    10. The GlobalState

      @@ -3541,7 +4034,7 @@

      9. The GlobalState

    -

    9.1. Singleton versus Shared State

    +

    10.1. Singleton versus Shared State

    From within several places deep in the code of the test scripts, we need access to a certain state of the system and act accordingly. The main state to access is the Setup which provides all configuration and calibration parameters of the test equipment and of the SUT.

    @@ -3556,12 +4049,12 @@

    9.1. Singleton versus Shared State

    -

    9.2. What is in the GlobalState?

    +

    10.2. What is in the GlobalState?

    The following sections describe the different states and function provided by the GlobalState. Remember the GlobalState is intended to be used within functions and methods of the test scripts in order to access global state information. That means the GlobalState needs to be initialised for some functions to work properly. Don’t use the GlobalState in Common-EGSE modules. If you need access to the Setup from the CGSE, explicitly request the Setup from the configuration manager using the get_setup() function.

    -

    9.2.1. The Setup

    +

    10.2.1. The Setup

    You can access the Setup from the GlobalState as follows:

    @@ -3618,13 +4111,13 @@

    9.2.1. The Setup

    -

    9.2.2. Performing a Dry Run

    +

    10.2.2. Performing a Dry Run

    At some point we need to check the test scripts that we write in order to see if the proper commands will be executed with their intended arguments. But we don’t want the commands to be sent to the mechanisms or controllers. We want to do a dry run where the script is executed as normal, but no instructions are sent to any device.

    -

    9.2.3. Retrieve the Command Sequence

    +

    10.2.3. Retrieve the Command Sequence

    Whenever a building block is executed, a command sequence is generated and stored in the GlobalState. There are two functions that access this command sequence: (1) the execute() function will copy the command sequence into the test dataset, and (2) the generate_command_sequence() will return the command sequence as a list (TODO: this will probably get its own class eventually).

    @@ -3633,7 +4126,7 @@

    9.2.3. Retrieve the Command Sequence

    -

    10. Data Strategy

    +

    11. Data Strategy

      @@ -3660,7 +4153,7 @@

      10. Data Strategy

    -

    11. Date, Time and Timestamps

    +

    12. Date, Time and Timestamps

    This section explains how and where time is used in the CGSE code and in the data.

    @@ -3697,7 +4190,7 @@

    11. Date, Time and Timestamps

    -

    11.1. Formatting the date and time

    +

    12.1. Formatting the date and time

    In the egse.system module we have provided a few functions to work with datetime in a consistent way. The most important is the format_datetime() function which is used for instance to create the timestamps for all CSV housekeeping data files.

    @@ -3735,7 +4228,7 @@

    11.1. Formatting the date and time

    Part III — Device Commanding

    -

    12. Device Control Servers

    +

    13. Device Control Servers

      @@ -3754,7 +4247,7 @@

      12. Device Control Servers

    -

    12.1. The Device Interface Classes

    +

    13.1. The Device Interface Classes

    • @@ -3781,7 +4274,7 @@

      12.1. The Device Interface Classes

    -

    12.1.1. The Connection Interface

    +

    13.1.1. The Connection Interface

    A connection interface defines the commands that are used to establish a connection, terminate a connection, and check if a connection has been established. We have two main connection interfaces: (1) a connection to a hardware device, e.g. a temperature controller or a filter wheel, and (2) a connection to a control server. A control server is the single point access to a hardware device.

    @@ -3905,7 +4398,7 @@
    Connection Interface for
    -

    13. The System Under Test (SUT)

    +

    14. The System Under Test (SUT)

      @@ -3930,7 +4423,7 @@

      13. The System Under Test (SUT)

    -

    13.1. DPU Control Server and DPU Processor

    +

    14.1. DPU Control Server and DPU Processor

    The DPU Control Server (used to be called the DPU Simulator in older documents) acts like any other control server. It’s Protocol class (DPUProtocol) starts a DPUController which implements the commands that are defined in the DPUInterface class. Specific DPU commands are sent to the control server using the DPUProxy class. The DPUProxy class also implements the DPUInterface.

    @@ -4037,7 +4530,7 @@

    13.1. DPU Control Server and DPU Processor

    -

    13.1.1. The Inner Loop

    +

    14.1.1. The Inner Loop

    The inner loop is the workhorse of the DPUProcessor, it has three main functions (1) reading information and data that is provided by the N-FEE, (2) sending that data to the storage manager, and (3) send commands to the N-FEE. In pseudo language, the inner loop performs the following functions:

    @@ -4121,13 +4614,13 @@

    13.1.1. The Inner Loop

    -

    13.2. The N-FEE Simulator

    +

    14.2. The N-FEE Simulator

    TBW

    -

    13.3. CCD Numbering

    +

    14.3. CCD Numbering

    Throughout the PLATO camera test campaigns, the CCD numbering, used throughout the CGSE, will remain the same (see figure below), but the underlying numbering, used by MSSL for the N-FEE will be different for EM, PFM, and FM.

    @@ -4137,7 +4630,7 @@

    13.3. CCD Numbering

    -

    13.3.1. MSSL Numbering

    +

    14.3.1. MSSL Numbering

    The MSSL numbering of the CCDs is used in:

    @@ -4153,7 +4646,7 @@

    13.3.1. MSSL Numbering

    -

    13.3.2. Conversions MSSL ⇆ CGSE

    +

    14.3.2. Conversions MSSL ⇆ CGSE

    Originally, the conversion back and forth between the MSSL and CGSE numbering was configured in egse.fee in the following variables:

    @@ -4178,18 +4671,18 @@

    13.3.2. Conversions MSSL ⇆ CGSE

    -

    13.3.3. Register map

    +

    14.3.3. Register map

    -

    13.3.4. Moving to the Setup

    +

    14.3.4. Moving to the Setup

    The idea is that these conversions will be moved to the setup file, under setup.camera.fee.ccd_numbering. The name of the relevant parameters is the same as above, but in small case.


    -

    13.3.5. EM

    +

    14.3.5. EM

    The CCD numbering in the CGSE and the connections to the PCBs for EM are shown in the figure below. Between brackets are the CCD numbers by MSSL. Not only do the CCD numbers not match between MSSL and the CGSE, but the connections between the CCDs and the corresponding PCBs is incorrect. The latter will be corrected for in PFM.

    @@ -4299,7 +4792,7 @@
    CGSE Configuration
    -

    13.3.6. PFM

    +

    14.3.6. PFM

    For PFM, the following modifications will be applied w.r.t. EM:

    @@ -4407,7 +4900,7 @@
    CGSE Configuration
    -

    13.3.7. FM

    +

    14.3.7. FM

    The CCD numbering and the connections to the PCBs for FM are the same as for PFM.

    @@ -4499,7 +4992,216 @@
    Default readout order
    -

    14. Device Simulators

    +

    15. SpaceWire

    +
    +
    +

    This chapter describes all facets of the SpaceWire interface from and to the camera. We use the 4Links Diagnostic SpaceWire Interface (DSI) with one port to connect to the N-CAM and 4 ports to connect to the F-CAM. The following topics will be explained in this chapter.

    +
    +
    +
      +
    • +

      The SpaceWire Lab interface

      +
    • +
    • +

      SpaceWireInterface class

      +
      +
        +
      • +

        SpaceWireOverDSI

        +
      • +
      • +

        SpaceWireOverZeroMQ

        +
      • +
      +
      +
    • +
    • +

      Connecting to the device

      +
      +
        +
      • +

        Speed

        +
      • +
      • +

        active link

        +
      • +
      +
      +
    • +
    • +

      testing your connection

      +
    • +
    +
    +
    +

    15.1. The SpaceWire Lab Interface – Hardware

    +
    +

    In the lab we use a SpaceWire interface from 4Links. This company provides devices to communicate over SpaceWire, but also to monitor, diagnose, develop your SpaceWire connections.

    +
    +
    +

    Two of these devices are used in the different institutes that test the PLATO cameras, i.e. the Diagnostic SpaceWire Interface (DSI) and the portable version of that (P-DSI). These devices have 2, 4, or 8 SpaceWire ports that can work concurrently at a speed up to 400Mbps. A 1Gbps Ethernet port connects your server to the DSI. 4Links provides software to drive the DSI and communicate with the camera over SpaceWire. The main component is a dynamic shared library EtherSpaceLink which is a C library that provides functionality to read and write SpaceWire packets from the active ports.

    +
    +
    +

    A Python wrapper module exists and is also delivered by 4Links, but at the time we were developing the CGSE, this Python interface was not available yet. We have implemented our own wrapper functions using the ctypes Python module.

    +
    +
    +

    The setup that is used in the lab is shown in the figure below. The N-CAM is connected to SpW port 1 on the DSI and the egse-server is connected to the Ethernet port.

    +
    +
    +
    +spw setup 01 +
    +
    +
    +

    The IP address of the DSI is different for each test house and is therefore configured in the local Settings file. The DSI is listening on TCP port 4949.

    +
    +
    +
    +

    15.2. The SpaceWire Interface class – Software

    +
    +

    The SpaceWireInterface class is the high-level base class that is used to communicate with a SpaceWire device. This class is intended as an interface class and shall not be used directly. We have provided two concrete sub-classes that inherit from the SpaceWireInterface, i.e. the SpaceWireOverDSI and the SpaceWireOverZeroMQ. The interface defines the following methods:

    +
    +
    +
    +
    connect()
    +
    +

    open a connection to the SpaceWire interface. All parameters that are needed to establish the connection must be passed when instantiating the concrete sub-class.

    +
    +
    disconnect()
    +
    +

    disconnect from the SpaceWire interface.

    +
    +
    configure()
    +
    +

    configure the SpaceWire device interface. All parameters that are needed to configure the connection must be passed when instantiating the concrete sub-class.

    +
    +
    flush()
    +
    +

    flush any remaining data from the wire.

    +
    +
    send_timecode(timecode: int)
    +
    +

    send a timecode packet.

    +
    +
    read_packet(timeout: int = None)
    +
    +

    read a SpaceWire packet. The packet is returned as a bytes object and can be converted using the factory method SpaceWirePacket.create_packet().

    +
    +
    write_packet(data: bytes)
    +
    +

    write a SpaceWire packet.

    +
    +
    read_register(self, address: int, length: int = 4, strict: bool = True)
    +
    +

    read data from the register map of the camera. There are restrictions on the address space and the number of bytes that can be read. The strict=True argument enforces these restrictions.

    +
    +
    write_register(self, address: int, data: bytes)
    +
    +

    write a register to the given address location in the memory map of the camera. The data shall be at least 4 bytes (only 4 bytes will be written).

    +
    +
    read_memory_map(self, address: int, size: int)
    +
    +

    read data from the memory map of the camera. The size of the data that can be read is dependent on the address space from which the data is read.

    +
    +
    +
    +
    +

    Sub-classes of the interface class can implement additional methods that are specific for that device, but only the interface methods shall be used in processes like the DPU Processor and the FEE Simulator.

    +
    +
    +

    The interface class can also be used as a context manager where it will open the connection upon entry and close the connection upon exit or when an exception happens.

    +
    +
    +
    +
    with SpaceWireOverDSI(dsi_address) as transport:
    +    transport.configure()
    +
    +    while True:
    +        flags, data = transport.read_packet(timeout=2000)
    +
    +        if 0 <= len(data) <= 1:
    +            break
    +
    +        ...
    +
    +
    +
    +

    Note that the connection is only established by entering the with statement, so, what usually is done is that the SpaceWireInterface sub-class is instantiated at a higher level in a caller function and past into the functions that use it.

    +
    +
    +
    +
    transport = SpaceWireOverZeroMQ(endpoint="tcp://localhost:5555", identity="PEER-TO-PEER")
    +
    +...
    +
    +def serve(transport: SpaceWireInterface):
    +
    +    with transport:
    +        transport.configure()
    +        ...
    +
    +
    +
    +

    15.2.1. Connecting to the DSI

    +
    +

    TBW

    +
    +
    +
      +
    • +

      Set/Get speed

      +
    • +
    • +

      Set/Get active port

      +
    • +
    • +

      Low-level functions: esl_*

      +
    • +
    +
    +
    +
    +
    +

    15.3. Testing your connection

    +
    +

    TBW

    +
    +
    +

    Using the unit tests…​.

    +
    +
    +
    +

    15.4. The RMAP Protocol

    +
    +

    TBW

    +
    +
    +

    Give a brief overview of the RMAP protocol as it is used in PLATO and describe how RMAP and DSI commands and functions work together.

    +
    +
    +
      +
    • +

      ReadRequest and ReadRequestReply

      +
    • +
    • +

      WriteRequest and WriteRequestReply

      +
    • +
    • +

      …​

      +
    • +
    +
    +
    +
    +

    15.5. A more elaborate example

    +
    +

    In this section we will present a more in-depth example of both connections of the SpaceWireInterface, i.e. the sending and the receiving part.

    +
    +
    +
    +
    +
    +

    16. Device Simulators

      @@ -4519,7 +5221,7 @@

      14. Device Simulators

    -

    14.1. The OGSE Simulator

    +

    16.1. The OGSE Simulator

    The OGSE simulator by default listens on TCP port 4181 for incoming connections from the OGSE Controller. The controller can be used stand-alone in a Python REPL for testing or can be part of the OGSE Control Server as the last step in the commanding chain.

    @@ -4699,13 +5401,13 @@

    14.1. The OGSE Simulator

    Part IV — Monitoring

    -

    15. Metrics, Prometheus and Grafana

    +

    17. Metrics, Prometheus and Grafana

    The CGSE provides mechanisms for monitoring metrics from different processes and devices with Grafana. This chapter will explain how the metrics are defined for your favorite process and device, how their values are gathered and propagated to the Prometheus server and how Grafana queries Prometheus to create timeseries that are displayed in your browser.

    -

    15.1. Setup Prometheus

    +

    17.1. Setup Prometheus

    • @@ -4718,7 +5420,7 @@

      15.1. Setup Prometheus

    -

    15.2. Define your metrics

    +

    17.2. Define your metrics

    Depending on your needs, you can choose different methods for the timestamp of your metrics. We will discuss three ways to get and use metrics timestamps:

    @@ -4780,11 +5482,13 @@

    15.2. Define your metrics

    Part V — Test Scripts

    -This section of the development manual is about developing test scripts to run the PLATO camera performance tests. If you were looking for unit tests, see Part VI — Unit Testing and beyond. +
    +

    This section of the development manual is about developing test scripts to run the PLATO camera performance tests. If you were looking for unit tests, see Part VI — Unit Testing and beyond.

    +
    -

    16. Observations

    +

    18. Observations

      @@ -4799,7 +5503,7 @@

      16. Observations

    -

    17. Building Blocks

    +

    19. Building Blocks

    @@ -4970,7 +5674,7 @@

    17. Building Blocks

    -

    18. The Tasks GUI

    +

    20. The Tasks GUI

    The Tasks GUI is a generic GUI that allows the test operator to execute standard tasks by clicking the task button and provide the necessary arguments. An example of such a Task GUI is given in the screenshot below.

    @@ -5006,7 +5710,7 @@

    18. The Tasks GUI

    In the rest of this chapter we will explain how these GUIs are created and how you can develop your own version for your specific tasks.

    -

    18.1. The package Layout

    +

    20.1. The package Layout

    To build up the Task GUI, we distinguish the task button as a function, several of these functions can be grouped in a Python module (a .py file) and all the modules plus additional information needed for the Task GUI is kept in a Python package. The CSL Operator GUI shown above, is located in the package camtest.csl and has the following layout:

    @@ -5030,7 +5734,7 @@

    18.1. The package Layout

    -

    18.2. Defining a Task

    +

    20.2. Defining a Task

    Let’s build our own simple Task GUI and start with the most stated and useless function, Hello, World!. We will eventually build a Task GUI with tasks of increasing complexity and guide you through the different steps.

    @@ -5156,7 +5860,7 @@

    18.2. Defining a Task

    -

    18.3. The @exec_ui decorator

    +

    20.3. The @exec_ui decorator

    TBW

    @@ -5184,7 +5888,7 @@

    18.3. The @exec_ui decorator

    -

    18.4. Type Hints and Defaults

    +

    20.4. Type Hints and Defaults

    TBW

    @@ -5206,7 +5910,7 @@

    18.4. Type Hints and Defaults

    -

    18.5. Choosing your icons

    +

    20.5. Choosing your icons

    TBW

    @@ -5225,7 +5929,7 @@

    18.5. Choosing your icons

    -

    18.6. The Jupyter Kernels

    +

    20.6. The Jupyter Kernels

    TBW

    @@ -5241,7 +5945,7 @@

    18.6. The Jupyter Kernels

    -

    18.7. Running your tasks

    +

    20.7. Running your tasks

    TBW

    @@ -5260,7 +5964,7 @@

    18.7. Running your tasks

    -

    18.8. The __init__.py

    +

    20.8. The __init__.py

    TBW

    @@ -5279,7 +5983,7 @@

    18.8. The __init__.py

    Part VI — Unit Testing and beyond

    -

    19. The Test Suite

    +

    21. The Test Suite

      @@ -5291,7 +5995,7 @@

      19. The Test Suite

    -

    20. Testing device Interfaces

    +

    22. Testing device Interfaces

    The first step in the development of a device driver is to implement the direct interface to the device, e.g. the Ethernet or USB interface. In parallel to this step start implementing the simulator

    @@ -5308,7 +6012,7 @@

    20. Testing device Interfaces

    -

    21. Code Reviews

    +

    23. Code Reviews

    @@ -5369,7 +6073,7 @@

    21. Code Reviews

    Please remember that the purpose of the code review is not to reject the code, but to improve the code quality. Focus is on how easy it is to understand the code.

    -

    21.1. Who is part of this Code Review?

    +

    23.1. Who is part of this Code Review?

    Developers: Rik Huygen, Sara Regibo, …​

    @@ -5381,7 +6085,7 @@

    21.1. Who is part of this Code Review?

    -

    21.2. Planning

    +

    23.2. Planning

    For each reviewer I will prepare an issue where you can check off the parts which have been reviewed. WHERE TO PUT THE REVIEW REPORTS/COMMENTS?

    @@ -5455,7 +6159,7 @@

    21.2. Planning

    -

    21.3. What needs to be reviewed?

    +

    23.3. What needs to be reviewed?

    TODO: Make a checklist!

    @@ -5498,7 +6202,7 @@

    21.3. What needs to be reviewed?

    -

    21.4. Prerequisites

    +

    23.4. Prerequisites

    Before the code review, all the code will be run through a number of automated steps:

    @@ -5533,11 +6237,13 @@

    21.4. Prerequisites

    Part VII — Miscellaneous

    -Everything in this part of the manual needs to be worked on and relocated in one of the above parts/sections. +
    +

    Everything in this part of the manual needs to be worked on and relocated in one of the above parts/sections.

    +
    -

    22. Set up your development environment

    +

    24. Set up your development environment

    This section guides you through the installation of the Common-EGSE, test scripts, and configuration files on your development machine. The result will be a development environment where you can implement features and fix issues in both CGSE and TS, and be able to push these changes to your origin[6] after which you can make a pull request to the upstream repository (i.e. the IvS-KULeuven original).

    @@ -5555,7 +6261,7 @@

    22. Set up your development environment

    -

    22.1. Create a GitHub account and request access

    +

    24.1. Create a GitHub account and request access

    First thing to do is make sure you have access to GitHub and have a GitHub account (personal or professional) so we[7] can give you access to the relevant repositories. Therefore,

    @@ -5584,7 +6290,7 @@

    22.1. Create a GitHub accou

    -

    22.2. Fork the repositories on GitHub

    +

    24.2. Fork the repositories on GitHub

    @@ -5623,7 +6329,7 @@

    22.2. Fork the repositories on GitHub<
    -

    22.3. Clone GitHub repository

    +

    24.3. Clone GitHub repository

    Execute the following code in a terminal. It will create a folder where all git repositories reside, then clone your fork of the plato-common-egse. A new folder will be created for plato-common-egse.

    @@ -5679,7 +6385,7 @@

    22.3. Clone GitHub repository

    -

    22.4. Set up the environment variables

    +

    24.4. Set up the environment variables

    Add the following lines to a file that can be sourced into your terminal session, e.g. ~/.bash_profile. Preferably, you should use modules to maintain your environment.

    @@ -5706,7 +6412,7 @@

    22.4. Set up the environment variable

    -

    22.5. Create a Python virtual environment

    +

    24.5. Create a Python virtual environment

    Create a virtual environment for Python 3.8 (the version to be used for the CGSE and TS), then activate the venv and update pip and setuptools packages.

    @@ -5746,7 +6452,7 @@

    22.5. Create a Python virtual envi

    -

    22.6. Install the CGSE

    +

    24.6. Install the CGSE

    It’s now time to finally install the Common-EGSE as a developer installation, also called editable mode (-e or --editable). This means the CGSE will be installed as a normal package, only the source code will be linked to the actual checked out source tree in you project. The result is that any changes that you make to the code in your source tree will be used in subsequent executions of scripts or in the Python REPL[8].

    @@ -5773,7 +6479,7 @@

    22.6. Install the CGSE

    -

    22.7. Start the core services

    +

    24.7. Start the core services

    From the project folder, run the invoke task start-core-egse to start up the five core services.

    @@ -5857,7 +6563,7 @@

    22.7. Start the core services

    -

    22.8. Updating the CGSE and TS

    +

    24.8. Updating the CGSE and TS

    Prepare for updates (only once)
    @@ -5934,13 +6640,13 @@

    22.8. Updating the CGSE and TS

    -

    23. Terminal Commands

    +

    25. Terminal Commands

    A lot of testing and monitoring can be done from the command terminal and doesn’t need a specific GUI or PyCharm. This section will explain how to check status of most of the control servers, how to inspect log files and HDF5 files, how to generate reports and how you can update the Common-EGSE and the test scripts.

    -

    23.1. What goes into this section

    +

    25.1. What goes into this section

    • @@ -5984,7 +6690,7 @@

      23.1. What goes into this section

    -

    23.2. Often used terminal commands

    +

    25.2. Often used terminal commands

    @@ -6021,7 +6727,7 @@

    23.2. Often used terminal commands

    -

    23.3. Often used git commands

    +

    25.3. Often used git commands

    The git commands need to be executed in the project folder or a sub-folder thereof, e.g. ~/git/plato-common-egse.

    @@ -6077,13 +6783,13 @@

    23.3. Often used git commands

    -

    24. Stories

    +

    26. Stories

    This section contains short stories of debugging sessions and working with the system that are otherwise difficult to explain in the previous sections.

    -

    24.1. Seemingly unrelated Process Manager Crash

    +

    26.1. Seemingly unrelated Process Manager Crash

    Related to issue: xxxx

    @@ -6185,7 +6891,7 @@

    24.1. Seemingly unrelated Pr

    -

    25. Miscellaneous

    +

    27. Miscellaneous

    @@ -6208,7 +6914,7 @@

    25. Miscellaneous

    -

    26. System Configuration

    +

    28. System Configuration

      @@ -6231,7 +6937,7 @@

      26. System Configuration

    -

    26.1. System Components

    +

    28.1. System Components

    This section describes the main components of the Common-EGSE system.

    @@ -6256,7 +6962,7 @@

    26.1. System Components

    -

    26.2. Data Flows

    +

    28.2. Data Flows

    The Common-EGSE provides functionality to configure, control and/or readout the following devices:

    @@ -6323,7 +7029,7 @@

    26.2. Data Flows

    -

    27. Installation and Update

    +

    29. Installation and Update

    The Common-EGSE software system is installed and updated via GitHub. The installation is more complex than a simple download-and-install procedure and is fully described in its installation guide [RD-01].

    @@ -6331,13 +7037,13 @@

    27. Installation and Update

    -

    28. Getting Started

    +

    30. Getting Started

    If you work with the system for the first time, you should go through the user manual [RD-02] to get familiar with the Common-EGSE setup, services and interactions. This section will explain how to log onto the Common-EGSE and how to prepare your development environment.

    -

    28.1. Log on to the System

    +

    30.1. Log on to the System

    • @@ -6356,7 +7062,7 @@

      28.1. Log on to the System

    -

    28.2. Setting up the development environment

    +

    30.2. Setting up the development environment

    • @@ -6412,8 +7118,8 @@

      28.2. Setting up the developmen

    diff --git a/docs/asciidocs/icd.html b/docs/asciidocs/icd.html index bd5a8a1..ee09554 100644 --- a/docs/asciidocs/icd.html +++ b/docs/asciidocs/icd.html @@ -4,7 +4,7 @@ - + Common-EGSE : Interface Control Document @@ -85,10 +85,10 @@ ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} ul,ol{margin-left:1.5em} ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0} -ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} -ul.square{list-style-type:square} ul.circle{list-style-type:circle} ul.disc{list-style-type:disc} +ul.square{list-style-type:square} +ul.circle ul:not([class]),ul.disc ul:not([class]),ul.square ul:not([class]){list-style:inherit} ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} dl dt{margin-bottom:.3125em;font-weight:bold} dl dd{margin-bottom:1.25em} @@ -209,13 +209,10 @@ .admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere} .admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} .exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px} -.exampleblock>.content>:first-child{margin-top:0} -.exampleblock>.content>:last-child{margin-bottom:0} .sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px} -.sidebarblock>:first-child{margin-top:0} -.sidebarblock>:last-child{margin-bottom:0} .sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} -.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} +.exampleblock>.content>:first-child,.sidebarblock>.content>:first-child{margin-top:0} +.exampleblock>.content>:last-child,.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} .literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em} @media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}} @media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}} @@ -394,7 +391,7 @@ dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility} h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em} p strong,td.content strong,div.footnote strong{letter-spacing:-.005em} -p,blockquote,dt,td.content,span.alt,summary{font-size:1.0625rem} +p,blockquote,dt,td.content,td.hdlist1,span.alt,summary{font-size:1.0625rem} p{margin-bottom:1.25rem} .sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} .exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc} @@ -439,73 +436,113 @@ @@ -684,24 +721,24 @@

    Conventions used in this Book

    -
    -
    +
    +
    +
      -
    • +
    • FM

    • -
    • +
    • EM

    -
    -
    +

    In this TAB we present FM specific information.

    -
    +

    In this TAB we present EM specific information.

    @@ -1024,26 +1061,26 @@

    4.2. The format of the HDF5 files

    Inspecting the HDF5 files can be done with the h5py module or you can use the CGSE module egse.h5 which provides convenience functions to work with HDF5 files. We normally use the CGSE module to explore the HDF5 files, but will provide equivalent code for inspection with the h5py module where possible.

    -
    -
    +
    +
    +
      -
    • +
    • egse.h5

    • -
    • +
    • h5py

    -
    -
    +
    >>> from egse import h5
    -
    +
    >>> import h5py
    @@ -1055,19 +1092,19 @@

    4.2. The format of the HDF5 files

    Let’s take an example file from IAS taken on 7th February 2023. The file is loaded with the h5.get_file() function and we can visualize the top level structure as follows:

    -
    -
    +
    +
    +
      -
    • +
    • egse.h5

    • -
    • +
    • h5py

    -
    -
    +
    >>> h5_fd = h5.get_file("20230207_IAS_N-FEE_SPW_06174.hdf5")
    @@ -1085,7 +1122,7 @@ 

    4.2. The format of the HDF5 files

    -
    +
    >>> h5_fd = h5py.File("20230207_IAS_N-FEE_SPW_06174.hdf5")
    @@ -1099,19 +1136,19 @@ 

    4.2. The format of the HDF5 files

    We can see that there are five top-level groups and three datasets. This is data taken in external sync mode, so we have four readouts per cycle. The data from each readout is in the groups 0, 1, 2, and 3. These numbers correspond to the frame number. Each of these groups has the following structure:

    -
    -
    +
    +
    +
      -
    • +
    • egse.h5

    • -
    • +
    • h5py

    -
    -
    +
    >>> h5.show_groups(h5_fd["/0"], max_level=1)
    @@ -1122,7 +1159,7 @@ 

    4.2. The format of the HDF5 files

    -
    +
    >>> [x for x in h5_fd["/0"]]
    @@ -1135,19 +1172,19 @@ 

    4.2. The format of the HDF5 files

    The data group contains all the SpaceWire packets that have image data, i.e. normal data packets and overscan packets. The data group also has the following attributes that are used to decode the SpaceWire packets into image data arrays. We will describe the data groups into more detail later in this section.

    -
    -
    +
    +
    +
      -
    • +
    • egse.h5

    • -
    • +
    • h5py

    -
    -
    +
    >>> h5.show_attributes(h5_fd["/0/data"])
    @@ -1167,7 +1204,7 @@ 

    4.2. The format of the HDF5 files

    -
    +
    >>> [x for x in h5_fd["/0/data"].attrs]
    @@ -1193,19 +1230,19 @@ 

    4.2. The format of the HDF5 files

    The two datasets in group '/0' contain the timecode and the housekeeping information that is sent on every sync pulse. The timecode dataset contains the timecode itself and the timestamp when this timecode was received by the DPU Processor. Remember the timecode is an integer from 0 to 63. The timecode dataset is an array with one integer element, the timestamp is an attribute of the timecode dataset. The timecode dataset and the timestamp can be visualised as follows.

    -
    -
    +
    +
    +
      -
    • +
    • egse.h5

    • -
    • +
    • h5py

    -
    -
    +
    >>> h5.get_data(h5_fd["/0/timecode"])
    @@ -1215,7 +1252,7 @@ 

    4.2. The format of the HDF5 files

    -
    +
    >>> h5_fd["/0/timecode"][()]
    @@ -1230,19 +1267,19 @@ 

    4.2. The format of the HDF5 files

    The raw content of the hk dataset can be shown as follows. The hk dataset has no attributes currently.

    -
    -
    +
    +
    +
      -
    • +
    • egse.h5

    • -
    • +
    • h5py

    -
    -
    +
    >>> h5.get_data(h5_fd["/0/hk"])
    @@ -1261,7 +1298,7 @@ 

    4.2. The format of the HDF5 files

    -
    +
    >>> h5_fd["/0/hk"][()]
    @@ -1285,19 +1322,19 @@ 

    4.2. The format of the HDF5 files

    The CGSE provides a module to inspect and work with PLATO SpaceWire packets. The above housekeeping packet can be inspected using the HousekeepingPacket class from the egse.spw package:

    -
    -
    +
    +
    +
      -
    • +
    • egse.h5

    • -
    • +
    • h5py

    -
    -
    +
    >>> from egse.spw import HousekeepingPacket
    @@ -1317,7 +1354,7 @@ 

    4.2. The format of the HDF5 files

    -
    +

    In this case only the retrieving of the hk_data is different:

    @@ -1361,19 +1398,19 @@

    4.2. The format of the HDF5 files

    We haven’t inspected the versions group yet, it currently contains only one dataset, format_version. This version describes the changes in the HDF5 file with respect to available groups, datasets and attributes. The format version can be accessed as follows.

    -
    -
    +
    +
    +
      -
    • +
    • egse.h5

    • -
    • +
    • h5py

    -
    -
    +
    >>> h5.show_attributes(h5_fd["/versions/format_version"])
    @@ -1383,7 +1420,7 @@ 

    4.2. The format of the HDF5 files

    -
    +
    >>> list(h5_fd["/versions/format_version"].attrs)
    @@ -1413,19 +1450,19 @@ 

    4.2. The format of the HDF5 files

    Before we dive into the data groups, let’s first inspect the three remaining datasets dpu, obsid and register. The obsid dataset contains the full observation identifier where this HDF5 file belongs to as a bytes object. If the obsid is empty, no observation was running.

    -
    -
    +
    +
    +
      -
    • +
    • egse.h5

    • -
    • +
    • h5py

    -
    -
    +
    >>> h5.get_data(h5_fd["/obsid"]).item()
    @@ -1433,7 +1470,7 @@ 

    4.2. The format of the HDF5 files

    -
    +
    >>> h5_fd["/obsid"][()]
    @@ -1446,19 +1483,19 @@ 

    4.2. The format of the HDF5 files

    The dpu dataset contains DPU Processor specific parameters that are needed to properly process the data. These parameters are available as attributes to this dataset and are mainly used by the FITS generation process.

    -
    -
    +
    +
    +
      -
    • +
    • egse.h5

    • -
    • +
    • h5py

    -
    -
    +
    >>> h5.show_attributes(h5_fd["/dpu"])
    @@ -1468,7 +1505,7 @@ 

    4.2. The format of the HDF5 files

    -
    +
    >>> list(h5_fd["/dpu"].attrs)
    @@ -1483,19 +1520,19 @@ 

    4.2. The format of the HDF5 files

    Finally, the register dataset is a Numpy array that is a mirror of the register memory map in the N-FEE at the time of the sync pulse.

    -
    -
    +
    +
    +
      -
    • +
    • egse.h5

    • -
    • +
    • h5py

    -
    -
    +
    >>> h5.get_data(h5_fd["/register"])
    @@ -1503,7 +1540,7 @@ 

    4.2. The format of the HDF5 files

    -
    +
    >>> h5_fd["/register"][()]
    @@ -2299,50 +2336,131 @@ 

    6. List of Files

    diff --git a/docs/asciidocs/installation-manual.html b/docs/asciidocs/installation-manual.html index 214be4e..1123b3c 100644 --- a/docs/asciidocs/installation-manual.html +++ b/docs/asciidocs/installation-manual.html @@ -4,7 +4,7 @@ - + Common-EGSE : Installation Manual @@ -85,10 +85,10 @@ ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} ul,ol{margin-left:1.5em} ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0} -ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} -ul.square{list-style-type:square} ul.circle{list-style-type:circle} ul.disc{list-style-type:disc} +ul.square{list-style-type:square} +ul.circle ul:not([class]),ul.disc ul:not([class]),ul.square ul:not([class]){list-style:inherit} ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} dl dt{margin-bottom:.3125em;font-weight:bold} dl dd{margin-bottom:1.25em} @@ -209,13 +209,10 @@ .admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere} .admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} .exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px} -.exampleblock>.content>:first-child{margin-top:0} -.exampleblock>.content>:last-child{margin-bottom:0} .sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px} -.sidebarblock>:first-child{margin-top:0} -.sidebarblock>:last-child{margin-bottom:0} .sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} -.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} +.exampleblock>.content>:first-child,.sidebarblock>.content>:first-child{margin-top:0} +.exampleblock>.content>:last-child,.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} .literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em} @media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}} @media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}} @@ -394,7 +391,7 @@ dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility} h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em} p strong,td.content strong,div.footnote strong{letter-spacing:-.005em} -p,blockquote,dt,td.content,span.alt,summary{font-size:1.0625rem} +p,blockquote,dt,td.content,td.hdlist1,span.alt,summary{font-size:1.0625rem} p{margin-bottom:1.25rem} .sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} .exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc} @@ -439,73 +436,113 @@ @@ -515,8 +552,8 @@

    Common-EGSE : Installation Manual

    Sara Regibo, Rik Huygen
    -version 1.4, -19/06/2023 +version 1.5, +15/09/2023
    Table of Contents
    @@ -571,36 +608,54 @@

    Common-EGSE : Installation Manual

  • 8. Installation of the Common-EGSE
  • 9. Installing the Test Scripts
  • 10. Installation of Setups
  • -
  • 11. Installation of Desktop Entries
  • -
  • 12. Installation of a VNC Server +
  • 11. User Installation +
  • +
  • 12. Installation of Desktop Entries
  • +
  • 13. Installation of a VNC Server + +
  • +
  • 14. Setting up the environment
  • -
  • 14. Setting up ssh access and GitHub deploy keys +
  • 15. Setting up ssh access and GitHub deploy keys
  • -
  • 15. Update the Common-EGSE to the latest release
  • -
  • 16. Update the Test Scripts to the latest release
  • -
  • 17. Update Python packages
  • -
  • 18. Data Propagation
  • -
  • 19. Shared Libraries
  • -
  • 20. Installating External Tools +
  • 16. Update the Common-EGSE to the latest release
  • +
  • 17. Update the Test Scripts to the latest release
  • +
  • 18. Update Python packages
  • +
  • 19. Data Propagation
  • +
  • 20. Shared Libraries
  • +
  • 21. Installating External Tools
  • @@ -621,6 +676,16 @@

    Changelog

    +
    15/09/2023 — v1.5
    +
    +
    +
      +
    • +

      added a section on user installation of CGSE + TS, see Chapter 11

      +
    • +
    +
    +
    18/06/2023 — v1.4
    @@ -636,7 +701,7 @@

    Changelog

    • -

      Added description for the PLATO_CAMERA_IS_EM environment variable, see Chapter 13

      +

      Added description for the PLATO_CAMERA_IS_EM environment variable, see Chapter 14

    • Added section on providing sudo permissions to plato-data in the user administration, see Section 2.3.

      @@ -661,19 +726,19 @@

      Changelog

      We will now try to maintain a changelog.

    • -

      Documented the installation and activation of a VNC Server on Ubuntu, see Chapter 12

      +

      Documented the installation and activation of a VNC Server on Ubuntu, see Chapter 13

    • -

      Improved section on 'Setting up the environment', see Chapter 13

      +

      Improved section on 'Setting up the environment', see Chapter 14

    • -

      Updated the version numbering used for releases in update commands, see Chapter 15 and Chapter 16

      +

      Updated the version numbering used for releases in update commands, see Chapter 16 and Chapter 17

    • Added info on how to set the terminal title, see [set-terminal-title]

    • -

      Added explanation of --ignore-installed optional argument for pip, see Chapter 17

      +

      Added explanation of --ignore-installed optional argument for pip, see Chapter 18

    • Added a brief explanation on exporting a mount point on the egse-server Section 2.1.8 and mounting the mount point Section 2.2.1 on the egse-client.

      @@ -760,24 +825,24 @@

      Conventions used in this Book

    -
    -
    +
    +
    +
      -
    • +
    • FM

    • -
    • +
    • EM

    -
    -
    +

    In this TAB we present FM specific information.

    -
    +

    In this TAB we present EM specific information.

    @@ -920,19 +985,19 @@

    2.1.1. Server Hardware

    In the next sections, we will briefly explain how to install the egse-server and egse-client machines. The frame below explains where to download and perform a basic installation of CentOS 8 or Ubuntu 20.04.

    -
    -
    +
    +
    +
      -
    • +
    • CentOS 8

    • -
    • +
    • Ubuntu 20.04

    -
    -
    +

    Perform a normal/default installation of CentOS-8. Follow the default settings.

    @@ -950,7 +1015,7 @@

    2.1.1. Server Hardware

    -
    +
    • @@ -993,19 +1058,19 @@

      2.1.3. Installation of Python 3.8

      We will install Python from the official Python website, as described in Chapter 6. The procedure is as follows: We first install the development tools for CentOS and a number of devel packages that are needed for header files during compilation. We then get the Python source distribution from www.python.org, unpack, configure and compile. Use altinstall instead of install if you don’t want previous installations of Python to be overwritten. This will install python3.8 in /usr/local/bin. All the commands need to be executes as root.

      -
      -
      +
      +
      +
        -
      • +
      • CentOS 8

      • -
      • +
      • Ubuntu 20.04

      -
      -
      +
      yum -y groupinstall "Development Tools"
      @@ -1019,7 +1084,7 @@ 

      2.1.3. Installation of Python 3.8

      -
      +

      Python 3.8 comes installed with Ubuntu 20.04, no need to re-install it.

      @@ -1385,19 +1450,19 @@

      2.2.1. NFS mount the /data folder from the egse-server

      We will need to make this mount permanently available, also after rebooting the system. This can be done as a static mount or by using the autmounter daemon.

      -
      -
      +
      +
      +
        -
      • +
      • Static mount

      • -
      • +
      • Automounter

      -
      -
      +

      With fstab we will create a static mount for the /data location. We add the mount point to /etc/fstab which will automatically mount the folder at startup and it will remain mounted until system shutdown or explicitly unmounted.

      @@ -1417,7 +1482,7 @@

      2.2.1. NFS mount the /data folder from the egse-server

      -
      +

      The automounter daemon is controlled by the autofs service. In the master configuration file for autofs, add a new line to specify a direct map for the file /etc/auto.cgse:

      @@ -1871,7 +1936,7 @@

      9. Installing the Test Scripts

      The installation will be done as the plato-data user. We will clone the IvS-KULeuven plato-test-scripts repository. There is no need to fork because we will not develop from this account and will therefore not do any pushes or pull requests.

      -

      Create a ~/git folder in the home directory of the plato-data user, move into that directory and clone the GitHub repository. Then move into the plato-test-scripts folder that was created by the clone command. If this is a first-time installation, you must first create a deploy key for the plato-test-scripts, see Chapter 14.

      +

      Create a ~/git folder in the home directory of the plato-data user, move into that directory and clone the GitHub repository. Then move into the plato-test-scripts folder that was created by the clone command. If this is a first-time installation, you must first create a deploy key for the plato-test-scripts, see Chapter 15.

      @@ -1900,7 +1965,7 @@

      10. Installation of Setups

      Installation on the server

      -

      The plato-cgse-conf repository shall be cloned on the server in the folder ~/git where also the plato-common-egse resides. To clone the repo, you need access to this repository first, follow the steps described in Section 14.3. When you have executed that procedure, you should have the repository up-to-date on your system.

      +

      The plato-cgse-conf repository shall be cloned on the server in the folder ~/git where also the plato-common-egse resides. To clone the repo, you need access to this repository first, follow the steps described in Section 15.3. When you have executed that procedure, you should have the repository up-to-date on your system.

      @@ -1932,7 +1997,7 @@

      Installation on the server

      Installation on the client

      -

      For the client follow the same procedure to create a deploy key and clone the repository (see Section 14.3), but do not allow write access on GitHub and only create the remote updates (the upload remote is not used on the client).

      +

      For the client follow the same procedure to create a deploy key and clone the repository (see Section 15.3), but do not allow write access on GitHub and only create the remote updates (the upload remote is not used on the client).

      Alternatively, the folder ~/git/plato-cgse-conf can be mounted read-only from the server. make sure the location is also ~/git/plato-cgse-conf.

      @@ -1943,7 +2008,482 @@

      Installation on the client

      -

      11. Installation of Desktop Entries

      +

      11. User Installation

      +
      +
      +

      This section describes how a user of the system can install the Common-EGSE and Test Scripts in a non-operational environment. The idea of such an installation is to work with the system, get used to it and/or perform some analysis on the test data. The user is not supposed to make changes to the software. If that is the case, i.e. you are working with test scripts, and make contributions and changes to the test scripts, go and read the section Section 11.3.

      +
      +
      +

      Installation of the CGSE + TS requires a few steps to prepare. We recommend to use an existing Python 3.8 installation with venv to create a virtual environment[7] or use pyenv to manage your Python versions if Python 3.8 is not installed on your system. To manage your shell environment[8] you can define the PLATO environment variables in your ~/.bash_profile or use direnv to manage your environment variables on a per-directory basis. We will explain the different possibilities in the following sections.

      +
      +
      +

      The diagram below outlines the discussion in the next sections. There are two paths that we recommend new users to follow (1) using an existing Python installation and the standard virtual environment, or (2) using pyenv to manage both a new Python installation and virtual environment.

      +
      +
      +
      +user installation +
      +
      +
      +

      11.1. Set up your Python environment

      +
      +

      11.1.1. Installed Python version and venv

      +
      +

      The Common-EGSE and Test Scripts require Python 3.8. Higher versions might work but are not tested and there are some known problem with e.g. the astropy package. You can test which Python version is installed on your system as follows:

      +
      +
      +
      +
      $ python3 -V
      +Python 3.8.17
      +
      +
      +
      +

      If you have Python 3.8 running you are good to go, you can use this version with venv to create a virtual environment in your work folder. The work folder I refer to here is the folder where you want to perform your PLATO work and keep scripts at hand. In this folder we will create a virtual environment for Python 3.8.

      +
      +
      +
      +
      $ mkdir ~/plato-work
      +$ cd ~/plato-work
      +$ python3 -m venv venv --prompt='cgse-ts'
      +$ source venv/bin/activate
      +(cgse-ts) $
      +
      +
      +
      +

      Now you have a Python 3.8 virtual environment which is activated. You can use the python and pip commands to set up your CGSE+TS installation (for clarity, I left out the (cgse-ts) from the prompt in the lines below):

      +
      +
      +
      +
      $ pip install --upgrade pip setuptools wheel
      +$ pip install cgse-ts
      +$ python -m egse.version
      +CGSE version in Settings: 2023.37.0+CGSE
      +CGSE installed version = 2023.37.0
      +$ python -m camtest.version
      +CAMTEST version in Settings: 2023.37.0+TS
      +CAMTEST installed version = 2023.37.0
      +
      +
      +
      +
      +

      11.1.2. pyenv

      +
      +

      In case that you do not have Python 3.8 installed on your system, we recommend to use pyenv to install and manage different Python versions. +The purpose of using pyenv is to keep track of Python installations on your system and to maintain Python virtual environments.

      +
      +
      +
      + + + + +
      + + +
      A nice pyenv tutorial
      +The Real Python website contains a very nice introductory tutorial on pyenv going through the installation on different platforms and the basic usage of the software. This section is largely based on that tutorial. You can access it at https://realpython.com/intro-to-pyenv/. +
      +
      +
      +
      +
      Installing pyenv
      +
      +

      When you don’t have pyenv installed on your system, use the following guidelines. The pyenv can be installed easily in a normal user account. Since pyenv builds the Python installations from source, you will need to install some build dependencies to actually use pyenv.

      +
      +
      +
      +
      +
        +
      • +

        macOS

        +
      • +
      • +

        Ubuntu

        +
      • +
      +
      +
      +
      +

      Use brew to install build dependencies, then install pyenv using the pyenv-installer:

      +
      +
      +
      +
      $ brew update
      +$ brew install openssl readline sqlite3 xz zlib
      +
      +$ curl https://pyenv.run | bash
      +
      +
      +
      +
      +
      +

      Use the following command to install build dependencies, the install pyenv using the pyenv-installer:

      +
      +
      +
      +
      $ sudo apt-get install -y make build-essential libssl-dev zlib1g-dev \
      +libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev \
      +libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python-openssl
      +
      +$ curl https://pyenv.run | bash
      +
      +
      +
      +
      +
      +
      +

      After the installation, follow the instructions to properly configure your shell to use pyenv. Add the following three lines to your ~/.bashrc and reload your shell or restart a terminal.[9]

      +
      +
      +
      +
      export PATH="$HOME/.pyenv/bin:$PATH"
      +eval "$(pyenv init -)"
      +eval "$(pyenv virtualenv-init -)"
      +
      +
      +
      +
      +
      +

      Now you should have a properly installed pyenv and you will be able to see which Python versions are installed on your system. As you can see below I have several Python versions installed on my system. Never mind about that, the CGSE+TS need Python 3.8.

      +
      +
      +
      +
      $ pyenv versions
      +  system
      +  3.8.17
      +  3.9.17
      +  3.10.12
      +  3.11.4
      +
      +
      +
      +

      If you don’t have a Python 3.8 installation, use the following command to install the latest patch for this Python version:

      +
      +
      +
      +
      $ pyenv install 3.8
      +
      +
      +
      +

      You should always install any Python packages in a virtual environment. That will isolate your installation from any other software that you have installed, make sure the packages on which the CGSE+TS dependent are not shared (or worse) changed by other software installations. A virtual environment is set up as follows with pyenv:

      +
      +
      +
      +
      $ pyenv virtualenv 3.8 ts-3.8
      +$ mkdir -p ~/plato-work
      +$ cd ~/plato-work
      +$ pyenv local ts-3.8
      +$ pip install --upgrade pip setuptools wheel
      +$ pip install cgse-ts
      +
      +
      +
      +
      +
      Global and local virtual environments
      +
      +

      A virtual environment is in principle independent of the directory where your project lives or where you keep your personal working scripts. You can create a virtual environment inside your project folder as we did above in Section 11.1.1, but pyenv keeps its virtual environments at one location independent of the project root. Instead, pyenv maintains a concept of a global and a local virtual environment. A global virtual environment can be used from any location and is defined with the following command, for clarity, we first create a new virtual environment core-3.8, then we set it as global:

      +
      +
      +
      +
      $ pyenv virtualenv 3.8 core-3.8
      +$ pyenv global core-3.8
      +
      +
      +
      +

      A local virtual environment is associated with a directory and its sub-folders. Whenever you navigate into a directory structure that has a local virtual environment associated, the virtual environment will automatically be activated in your current shell. When you leave the directory structure, the virtual environment will be deactivated.

      +
      +
      +
      +
      $ cd ~/plato-work
      +$ pyenv local ts-3.8
      +$ pyenv local
      +ts-3.8
      +$ cd ..
      +$ pyenv local
      +pyenv: no local version configured for this directory
      +$ pyenv global
      +core-3.8
      +
      +
      +
      +

      So, the local takes precedence over the global.

      +
      +
      +
      +
      +
      +
      +

      11.2. Set up your shell environment

      +
      +

      11.2.1. Environment variables

      +
      +

      The non-operational installation needs the following environment variables to work without problems. You can define those environment variables in your ~/.bash_profile or ~/.bashrc, or you can use direnv to define them only for specific locations.

      +
      + ++++ + + + + + + + + + + + + + + + + + + + + + + + + +
      NameDescription

      PLATO_DATA_STORAGE_LOCATION

      the root folder of your data storage, usually /data or ~/data. The SITE is automatically appended by the CGSE and underneath the SITE folder, folders like obs, daily and log are located. So, for example, if you are working at INTA and your PLATO_DATA_STORAGE_LOCATION is set to /data, the observation data for OBSID=793 will be in /data/INTA/obs/00793_INTA_duvel. Even if you are not working on an operational machine, the directory structure underneath the PLATO_DATA_STORAGE_LOCATION shall be respected.

      PLATO_LOCAL_SETTINGS

      the location of the local-settings.yaml file. This settings file will overwrite the global settings defined by the distribution. In this file you shall put your SITE ID, IP addresses of devices, different port numbers etc. The structure is the same as the global settings.yaml file, only with less content. You can put this file in the data storage root folder or in a dedicated folder like ~/cgse.

      PLATO_CONF_DATA_LOCATION

      the location of the configuration data, i.e. Setups and related files, usually ~/git/plato-cgse-conf/data/{SITE}/conf [10]

      PLATO_CONF_REPO_LOCATION

      the location of the configuration data repository, usually ~/git/plato-cgse-conf.

      +
      +

      For more information on these variables, check the installation guide.

      +
      +
      +
      +

      11.2.2. direnv

      +
      +
      +
      Installing direnv
      +
      + + + + + +
      + + +The installation described below is based on the information from https://direnv.net +
      +
      +
      +

      The purpose of direnv is to set up environment variables on a per-folder basis. When you enter a folder with the cd command in your terminal, the shell will load a set of environment variables applicable for that folder. Let’s first install direnv on your system.

      +
      +
      +
      +
      +
        +
      • +

        macOS

        +
      • +
      • +

        Ubuntu

        +
      • +
      +
      +
      +
      +
      +
      $ brew install direnv
      +
      +
      +
      +
      +
      +
      +
      $ sudo apt-get install direnv
      +
      +
      +
      +
      +
      +
      +

      In order to activate direnv, append the following line at the end of your ~/.bashrc file:

      +
      +
      +
      +
      eval "$(direnv hook bash)"
      +
      +
      +
      +

      Make sure it appears even after any shell extensions that manipulate the prompt.

      +
      +
      +
      +
      +

      Now that direnv is installed, you can go to your project folder and create a .envrc file that contains the definitions of the environment variables needed for the CGSE+TS.

      +
      +
      +
      +
      $ cd ~/plato-work
      +$ cat > .envrc
      +SITE=CSL2
      +
      +export PLATO_CONF_DATA_LOCATION=~/git/plato-cgse-conf/data/${SITE}/conf
      +export PLATO_CONF_REPO_LOCATION=~/git/plato-cgse-conf
      +export PLATO_DATA_STORAGE_LOCATION=~/data
      +export PLATO_LOCAL_SETTINGS=~/cgse/local_settings.yaml
      +
      +export PYTHONSTARTUP=~/plato-work/startup.py
      +^D
      +$ direnv allow .
      +
      +
      +
      +

      That last command will load the environment variables in your shell. You can easily check this with the following command (try this both inside and outside of the project folder):

      +
      +
      +
      +
      $ set | grep PLATO
      +PLATO_CONF_DATA_LOCATION=/Users/rik/git/plato-cgse-conf/data/CSL2/conf
      +PLATO_CONF_REPO_LOCATION=/Users/rik/git/plato-cgse-conf
      +PLATO_DATA_STORAGE_LOCATION=/Users/rik/data
      +PLATO_LOCAL_SETTINGS=/Users/rik/cgse/local_settings.yaml
      +
      +
      +
      +
      +
      +

      11.3. Contributing to the PLATO Test Scripts

      +
      +

      In the previous sections we have installed the CGSE and TS as read-only packages in a virtual environment. That allowed you to use the software, but not to make changes or contribute to improve the software. This section will assume you need to contribute to the test scripts but not the Common-EGSE. Make sure you have a fork of the plato-test-scripts repository and clone this into a folder ~/git/plato-test-scripts.

      +
      +
      +
      +
      $ cd ~/git
      +$ git clone https://github.com/<username>/plato-test-scripts.git
      +$ cd plato-test-scripts
      +
      +
      +
      +

      Now create a virtual environment either with venv or with pyenv as explained in the sections above. I will use venv here assuming Python 3.8 is installed.

      +
      +
      +
      +
      $ python3 -m venv venv --prompt='cgse-ts'
      +$ source venv/bin/activate
      +$ pip install --upgrade pip setuptools wheel
      +
      +
      +
      +

      Now install the test scripts as an editable installation. What will happen is that the following command will install the latest cgse package and all dependencies needed for CGSE and TS. It will then install the code in the src folder, e.g. the camtest and scripts as an editable install. Whenever you make changes to the code, it will be picked up if you run the code again.

      +
      +
      +
      +
      $ pip install -e .
      +
      +
      +
      +

      Check the versions installed for CGSE and TS:

      +
      +
      +
      +
      $ python -m egse.version
      +CGSE version in Settings: 2023.37.0+CGSE
      +CGSE installed version = 2023.37.0
      +$ python -m camtest.version
      +CAMTEST version in Settings: 2023.37.0+TS
      +CAMTEST git version: 2023.37.0+TS-0-g3ccd722
      +CAMTEST installed version = 2023.37.0
      +
      +
      +
      +
      +

      11.4. Contributing to PLATO Test Scripts and Common-EGSE

      +
      +

      In this last section we look into the case where you are making contributions to both the CGSE and TS. Of course, when you arrive at this point, I assume you have a better understanding of git and package distribution and I will not explain everything in detail. Basically, what this boils down to is that both CGSE and TS will be installed as editable packages in the same virtual environment.

      +
      +
      +

      Create a virtual environment:

      +
      +
      +
      +
      +
        +
      • +

        venv

        +
      • +
      • +

        pyenv

        +
      • +
      +
      +
      +
      +

      Put this virtual environment in a folder that is not the project folder of CGSE nor TS, i.e. use a global virtual environment, not one that is created inside the project.

      +
      +
      +
      +
      $ cd ~/venvs
      +$ python3.8 -m venv cgse-ts-3.8
      +$ source cgse-ts-3.8/bin/activate
      +
      +
      +
      +
      +
      +
      +
      $ pyenv virtualenv 3.8 cgse-ts-3.8
      +$ pyenv shell cgse-ts-3.8
      +
      +
      +
      +
      +
      +
      +

      Update the standard packages that are used for the installation:

      +
      +
      +
      +
      $ pip install --upgrade pip setuptools wheel
      +
      +
      +
      +

      Now navigate to the project folder of the CGSE and pip install this project as an editable package. Do the same for the TS project.

      +
      +
      +
      +
      $ cd ~/git/plato-common-egse
      +$ pip install -e .
      +$ cd ~/git/plato-test-scripts
      +$ pip install -e .
      +
      +
      +
      +

      If you now check your installed packages, you will see that both cgse and cgse-ts are installed as editable projects:

      +
      +
      +
      +
      $ pip list -v
      +Package             Version        Editable project location                               Location                                                                Installer
      +------------------- -------------- ------------------------------------------------------- ----------------------------------------------------------------------- ---------
      +appnope             0.1.3                                                                  /Users/rik/.pyenv/versions/3.8.17/envs/ts-x/lib/python3.8/site-packages pip
      +APScheduler         3.10.4                                                                 /Users/rik/.pyenv/versions/3.8.17/envs/ts-x/lib/python3.8/site-packages pip
      +astropy             4.0                                                                    /Users/rik/.pyenv/versions/3.8.17/envs/ts-x/lib/python3.8/site-packages pip
      +asttokens           2.4.0                                                                  /Users/rik/.pyenv/versions/3.8.17/envs/ts-x/lib/python3.8/site-packages pip
      +backcall            0.2.0                                                                  /Users/rik/.pyenv/versions/3.8.17/envs/ts-x/lib/python3.8/site-packages pip
      +backports.zoneinfo  0.2.1                                                                  /Users/rik/.pyenv/versions/3.8.17/envs/ts-x/lib/python3.8/site-packages pip
      +bcrypt              4.0.1                                                                  /Users/rik/.pyenv/versions/3.8.17/envs/ts-x/lib/python3.8/site-packages pip
      +certifi             2023.7.22                                                              /Users/rik/.pyenv/versions/3.8.17/envs/ts-x/lib/python3.8/site-packages pip
      +cffi                1.15.1                                                                 /Users/rik/.pyenv/versions/3.8.17/envs/ts-x/lib/python3.8/site-packages pip
      +cgse                2023.37.0      /Users/rik/Documents/PyCharmProjects/plato-common-egse  /Users/rik/.pyenv/versions/3.8.17/envs/ts-x/lib/python3.8/site-packages pip
      +cgse-ts             2023.37.0      /Users/rik/Documents/PyCharmProjects/plato-test-scripts /Users/rik/.pyenv/versions/3.8.17/envs/ts-x/lib/python3.8/site-packages pip
      +charset-normalizer  3.2.0                                                                  /Users/rik/.pyenv/versions/3.8.17/envs/ts-x/lib/python3.8/site-packages pip
      +click               8.1.7                                                                  /Users/rik/.pyenv/versions/3.8.17/envs/ts-x/lib/python3.8/site-packages pip
      +
      +
      +
      +

      Make sure you are in the correct virtual environment when working on the CGSE or TS.

      +
      +
      +
      +
      +
      +

      12. Installation of Desktop Entries

      This section explains how to install a Desktop icon for one of the GUI applications. In the screenshot below, you see the icon for the Mechanical Positions Hexapod GUI in the toolbar on the left side of the screen. Click that icon to start the Task GUI.

      @@ -2029,7 +2569,7 @@

      11. Installation of Desktop Entries

      -

      12. Installation of a VNC Server

      +

      13. Installation of a VNC Server

      This section describes how to install a VNC Server on your client machine. The VNC server will run on display=:0 and will allow remote users to connect to the main desktop that is used by the operator. This remote connection will then be able to provide support and in case of emergency take over the desktop from the operator.

      @@ -2038,9 +2578,9 @@

      12. Installation of a VNC Server

      We will install the packages x11vnc and net-tools, then we will configure the GNOME Display Manager (gdm3) and finally install and activate the X11 VNC service.

      -

      12.1. Install packages

      +

      13.1. Install packages

      -

      Use the following command to install the necessary packages. You can do this as user 'plato-data' and when asked for a password, provide the password of that user[7].

      +

      Use the following command to install the necessary packages. You can do this as user 'plato-data' and when asked for a password, provide the password of that user[11].

      @@ -2049,7 +2589,7 @@

      12.1. Install packages

      -

      12.2. Configure the GNOME Display Manager

      +

      13.2. Configure the GNOME Display Manager

      Edit the file /etc/gdm3/custom.conf to disable Wayland and enable automatic login by the 'plato-data' user. The changes are all in the daemon section of the configuration file. If some entries do not exist on your system, create them. You should edit this file as 'root' or with sudo under the 'plato-data' user.

      @@ -2066,7 +2606,7 @@

      12.2. Configure the GNOME Display

      -

      12.3. Install and Activate the VNC service

      +

      13.3. Install and Activate the VNC service

      To install the X11 VNC service under Systemd, create a file /etc/systemd/system/x11vnc.service with the following content.

      @@ -2112,10 +2652,10 @@

      12.3. Install and Activate the VN

      -

      13. Setting up the environment

      +

      14. Setting up the environment

      -

      13.1. Environment Variables

      +

      14.1. Environment Variables

      The environment variables that are used by the CGSE or test scripts are listed in the table below. It is best practice to define those variable in ~/.bash_profile to have them loaded on login. With the below definitions, a standard ~/.bash_profile would look like this (example taken from the CSL1 egse-client daiquiri)

      @@ -2193,7 +2733,7 @@

      13.1. Environment Variables

      /cgse/lib/python/:/cgse/lib/python3.8/site-packages

      -

      PYTHONPATH – egse-client[8]

      +

      PYTHONPATH – egse-client[12]

      /cgse/lib/python:/cgse/lib/python3.8/site-packages:/home/plato-data/git/plato-test-scripts/src:/home/plato-data/git/plato-test-scripts/venv/lib/python3.8/site-packages/

      @@ -2205,7 +2745,7 @@

      13.1. Environment Variables

      /data/<SITE_ID>/conf

      -

      PLATO_CONF_REPO_LOCATION[9]

      +

      PLATO_CONF_REPO_LOCATION[13]

      /home/plato-data/git/plato-cgse-conf

      @@ -2229,14 +2769,14 @@

      13.1. Environment Variables

      /home/plato-data/git/plato-common-egse/src/egse/lib/ximc/libximc.framework

      -

      PLATO_CAMERA_IS_EM[10]

      +

      PLATO_CAMERA_IS_EM[14]

      True | False

      -

      13.2. The environment for Systemd services

      +

      14.2. The environment for Systemd services

      For the CGSE core services, that are started by the Systemd on the egse-server, the environment variable are defined in the file /cgse/env.txt. The example below shows the content of this file from the CSL1 egse-server strawberry

      @@ -2255,13 +2795,13 @@

      13.2. The environment for Systemd

      -

      14. Setting up ssh access and GitHub deploy keys

      +

      15. Setting up ssh access and GitHub deploy keys

      -

      We have a semi-automatic update procedure in place for the Common-EGSE and the Test Scripts. This procedure will be described in Chapter 15 and Chapter 16, but before we can use that we need to have the proper permissions to fetch changes from the different GitHub repositories without the need to provide our credentials every time. The best way to do that is to set up deploy keys on the GitHub repos.

      +

      We have a semi-automatic update procedure in place for the Common-EGSE and the Test Scripts. This procedure will be described in Chapter 16 and Chapter 17, but before we can use that we need to have the proper permissions to fetch changes from the different GitHub repositories without the need to provide our credentials every time. The best way to do that is to set up deploy keys on the GitHub repos.

      -

      14.1. Create a deploy key for the plato-common-egse

      +

      15.1. Create a deploy key for the plato-common-egse

      You can create an ssh key-pair for the CGSE with the following command. Execute this code on the egse-server as user plato-data in the folder ~/.ssh (create the folder if it doesn’t exist).

      @@ -2370,7 +2910,7 @@

      14.1. Create a deploy key for the plato-common-e

      -

      14.2. Create a deploy key for the plato-test-scripts

      +

      15.2. Create a deploy key for the plato-test-scripts

      You can create an ssh key-pair for the test scripts with the following command. Execute this code on the egse-client as user plato-data in the folder ~/.ssh (create the folder if it doesn’t exist).

      @@ -2483,7 +3023,7 @@

      14.2. Create a deploy k

      -

      14.3. Create a deploy key for the plato-cgse-conf

      +

      15.3. Create a deploy key for the plato-cgse-conf

      You can create an ssh key-pair for the plato-cgse-conf with the following command. Execute this code on the egse-server as user plato-data in the folder ~/.ssh (create the folder if it doesn’t exist).

      @@ -2556,7 +3096,7 @@

      14.3. Create a deploy key for the plato-cgse-conf<

      -

      Check the permissions of the ~/.ssh directory and the files in it (see Section 14.1).

      +

      Check the permissions of the ~/.ssh directory and the files in it (see Section 15.1).

      If you have not yet cloned the plato-cgse-conf repository, you can do that now with the following command:

      @@ -2614,7 +3154,7 @@

      14.3. Create a deploy key for the plato-cgse-conf<

      -

      15. Update the Common-EGSE to the latest release

      +

      16. Update the Common-EGSE to the latest release

      At some point you will be asked to update to a specific release. Make sure you are in the develop branch, then execute the following commands:

      @@ -2646,7 +3186,7 @@

      15. Update the Common-EGSE to the latest release

      -

      16. Update the Test Scripts to the latest release

      +

      17. Update the Test Scripts to the latest release

      When you need to update the test scripts on your egse-client machine, use the following commands:

      @@ -2681,7 +3221,7 @@

      16. Update the Test Scripts to the latest release

      -

      17. Update Python packages

      +

      18. Update Python packages

      Ideally, the installed third party packages should be the versions that are given the requirements file of the project. If the requirements file is updated, you can use the following command to update your installation:

      @@ -2725,7 +3265,7 @@

      17. Update Python packages

      -

      18. Data Propagation

      +

      19. Data Propagation

        @@ -2784,7 +3324,7 @@

        18. Data Propagation

      -

      19. Shared Libraries

      +

      20. Shared Libraries

      Some components use a shared library that is loaded by the Python interpreter. In order to load the library, the absolute path to the shared object file must be known. Different modules handle this in their own way.

      @@ -2809,10 +3349,10 @@

      19. Shared Libraries

      -

      20. Installating External Tools

      +

      21. Installating External Tools

      -

      20.1. Cutelog GUI

      +

      21.1. Cutelog GUI

      Cutelog is a GUI that can be installed in your virtual environment using pip.

      @@ -2831,7 +3371,7 @@

      20.1. Cutelog GUI

      -

      20.2. Textualog TUI

      +

      21.2. Textualog TUI

      Textualog is a terminal application (TUI) that allows you to inspect the log files in $PLATO_LOG_FILE_LOCATION. The package is open-source and can be installed in your virtual environment from PyPI using pip:

      @@ -2841,7 +3381,7 @@

      20.2. Textualog TUI

      -

      Textualog is extremely useful to inspect and follow logging messages in a remote terminal. It is inpired on the cutelog app and developed specifically for the remote users. After installation, the current log file can be inspected with the following command[11]:

      +

      Textualog is extremely useful to inspect and follow logging messages in a remote terminal. It is inpired on the cutelog app and developed specifically for the remote users. After installation, the current log file can be inspected with the following command[15]:

      @@ -2878,72 +3418,165 @@

      20.2. Textualog TUI

      6. The latest release tag can be found on the GitHub pages of the repository
      -7. The 'plato-data' user is supposed to have sudo rights. +7. I will use the term virtual environment when I’m talking about a Python environment where packages are installed at a specific location for a specific Python version.
      -8. The PYTHONPATH on the client is different because it also has the plato-test-scripts installed, so in addition to the CGSE packages, it needs to have access to the test scripts Python code and packages that were installed as part of the plato-test-scripts virtual environment. +8. I will use the term shell environment when talking about an environment in your Terminal, i.e. usually bash or zsh where your environment variables are defined and you execute commands.
      -9. This variable only needs to be defined in the /cgse/env.txt file on the egse-server. +9. If you don’t have a ~/.bashrc file, append the lines to your ~/.bash_profile file.
      -10. PLATO_CAMERA_IS_EM was introduced in release 2022.3.2+CGSE to handle the difference between EM and pFM camera with respect to the format of image and housekeeping data provided by the FPGA. The EM camera data was in twos-complement. +10. {SITE} shall be replaced by the SITE_ID of your test house.
      -11. the textualog package might already installed on the egse-server at the test houses. +11. The 'plato-data' user is supposed to have sudo rights. +
      +
      +12. The PYTHONPATH on the client is different because it also has the plato-test-scripts installed, so in addition to the CGSE packages, it needs to have access to the test scripts Python code and packages that were installed as part of the plato-test-scripts virtual environment. +
      +
      +13. This variable only needs to be defined in the /cgse/env.txt file on the egse-server. +
      +
      +14. PLATO_CAMERA_IS_EM was introduced in release 2022.3.2+CGSE to handle the difference between EM and pFM camera with respect to the format of image and housekeeping data provided by the FPGA. The EM camera data was in twos-complement. +
      +
      +15. the textualog package might already installed on the egse-server at the test houses.
      diff --git a/docs/asciidocs/user-manual.html b/docs/asciidocs/user-manual.html index bb4dde5..feacd32 100644 --- a/docs/asciidocs/user-manual.html +++ b/docs/asciidocs/user-manual.html @@ -4,7 +4,7 @@ - + Common–EGSE & Test Scripts: User Manual @@ -85,10 +85,10 @@ ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit} ul,ol{margin-left:1.5em} ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0} -ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit} -ul.square{list-style-type:square} ul.circle{list-style-type:circle} ul.disc{list-style-type:disc} +ul.square{list-style-type:square} +ul.circle ul:not([class]),ul.disc ul:not([class]),ul.square ul:not([class]){list-style:inherit} ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0} dl dt{margin-bottom:.3125em;font-weight:bold} dl dd{margin-bottom:1.25em} @@ -209,13 +209,10 @@ .admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere} .admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} .exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px} -.exampleblock>.content>:first-child{margin-top:0} -.exampleblock>.content>:last-child{margin-bottom:0} .sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px} -.sidebarblock>:first-child{margin-top:0} -.sidebarblock>:last-child{margin-bottom:0} .sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center} -.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} +.exampleblock>.content>:first-child,.sidebarblock>.content>:first-child{margin-top:0} +.exampleblock>.content>:last-child,.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0} .literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em} @media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}} @media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}} @@ -394,7 +391,7 @@ dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility} h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em} p strong,td.content strong,div.footnote strong{letter-spacing:-.005em} -p,blockquote,dt,td.content,span.alt,summary{font-size:1.0625rem} +p,blockquote,dt,td.content,td.hdlist1,span.alt,summary{font-size:1.0625rem} p{margin-bottom:1.25rem} .sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em} .exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc} @@ -438,73 +435,113 @@ @@ -537,38 +574,37 @@

      Common–EGSE & Test Scripts: User Manual

    • 1.3. Client vs. Server
  • -
  • 2. Update the Common-EGSE Software
  • -
  • 3. Starting the Core Services
  • -
  • 4. The Graphical User Interfaces (GUI) +
  • 2. Starting the Core Services
  • +
  • 3. The Graphical User Interfaces (GUI)
  • -
  • 5. The Tasks GUI +
  • 4. The Tasks GUI
  • -
  • 6. Frequently Asked Questions (FAQ) +
  • 5. Frequently Asked Questions (FAQ)
  • @@ -589,12 +625,22 @@

    Changelog

    +
    12/02/2024 — v0.3
    +
    +
    +
      +
    • +

      Removed the chapter on updating the CGSE, see the installation guide for this.

      +
    • +
    +
    +
    28/06/2023 — v0.2
    • -

      added a section on the DPU GUI application, see Section 4.6

      +

      added a section on the DPU GUI application, see Section 3.6

    • removed section on contingency GUI since those tasks are now integrated in each of the TH GUIs using shared tasks

      @@ -613,7 +659,7 @@

      Changelog

      Introduce Changelog, this section

    • -

      Added an explanation how to execute tasks in the Jupyter Console, see Section 5.6.

      +

      Added an explanation how to execute tasks in the Jupyter Console, see Section 4.6.

    @@ -886,6 +932,10 @@

    Acronyms

    Camera

    +

    CCD

    +

    Charged-Coupled Device

    + +

    CGSE

    Common-EGSE

    @@ -894,6 +944,10 @@

    Acronyms

    Centre Spatial de Liège

    +

    CSV

    +

    Comma-Separated Values

    + +

    COT

    Commercial off-the-shelf

    @@ -902,6 +956,10 @@

    Acronyms

    Charge Transfer Inefficiency

    +

    CTS

    +

    Consent to Ship

    + +

    DPU

    Data Processing Unit

    @@ -918,10 +976,18 @@

    Acronyms

    End Of Life

    +

    FAQ

    +

    Frequently Asked Questions

    + +

    FEE

    Front End Electronics

    +

    FITS

    +

    Flexible Image Transport System

    + +

    FPA

    Focal Plane Assembly/Array

    @@ -930,6 +996,14 @@

    Acronyms

    Ground Support Equipment

    +

    GUI

    +

    Graphical User Interface

    + + +

    HDF5

    +

    Hierarchical Data Format version 5 (File format)

    + +

    HK

    Housekeeping

    @@ -962,6 +1036,10 @@

    Acronyms

    Non-Conformance Review Board

    +

    OBSID

    +

    Observation Identifier

    + +

    OGSE

    Optical Ground Support Equipment

    @@ -970,10 +1048,18 @@

    Acronyms

    Operating System

    +

    PDF

    +

    Portable Document Format

    + +

    PID

    Process Identifier

    +

    PLATO

    +

    PLAnetary Transits and Oscillations of stars

    + +

    PPID

    Parent Process Identifier

    @@ -1002,6 +1088,10 @@

    Acronyms

    SpaceWire

    +

    SQL

    +

    Structured Query Language

    + +

    SRON

    Stichting Ruimte-Onderzoek Nederland

    @@ -1050,13 +1140,25 @@

    Acronyms

    Test Scripts

    +

    TUI

    +

    Text-based User Interface

    + +

    TV

    Thermal Vacuum

    +

    UM

    +

    User Manual

    + +

    USB

    Universal Serial Bus

    + +

    YAML

    +

    YAML Ain’t Markup Language

    +
    @@ -1118,7 +1220,7 @@

    1.2. Where do I start?

    The red and green bullets in the PM GUI describe the state of the service or process. When the bullet is green the process is running as expected, when the bullet is red the process is not running. For some devices the bullet can become orange to indicate that the control server process is running, but the device is not connected and can therefore not be commanded. The icons at the right of the processes are for starting/stopping the process. Some processes can be started in simulator mode (toggle the device icon) and some of the processes have a GUI associated that can be started from the PM GUI. Please note that none of the core services can be started from the Process Manager GUI. Core services are managed by the Systemd services of your Linux system and when stopped or crashed they will be automatically restarted.

    -

    The Process Manager is fully explained in its own section The Process Manager GUI in Section 4.2.

    +

    The Process Manager is fully explained in its own section The Process Manager GUI in Section 3.2.

    The Process Manager presents you with a view of the state of the system and allows you to start and stop device processes and start some GUIs to manipulate certain devices and services. The main task for an operator to perform camera tests or alignment procedures is however to execute the test procedure as described in the TP-0011 Google Sheet for each of the test houses and in the ambient alignment procedure PR-0011 for CSL. These documents contain step-by-step as-run procedures that describe all manual interactions, tasks and code execution needed to successfully perform a camera test. Each of the rows in these procedures describe one action that needs to be successfully finished before proceeding to the next action. As said, this can be a manual interaction with a device, a task to be executed from a specific GUI, or a code snippet that needs to be executed in the dedicated environment. The Python source code from the as-run procedure will be executed in a Python Console that is used throughout the completion of the as-run procedure.

    @@ -1199,43 +1301,7 @@

    1.3.2. What should run on the Client?

    -

    2. Update the Common-EGSE Software

    -
    -
    -
    -
    update_cgse develop
    -
    -
    -
    -

    To update the Common-EGSE on the operational machine, use the ops argument instead. An operational installation is different from a developer installation. There is no virtual environment and the all required Python packages, including the Common-EGSE are installed at a specific location. The installation process makes use of the following environment variables:

    -
    -
    -
      -
    • -

      PLATO_COMMON_EGSE_PATH: the location of the plato-common-egse repository -on your machine, e.g., ~/git/plato-common-egse

      -
    • -
    • -

      PLATO_INSTALL_LOCATION: the location where the packages shall be -installed, usually /cgse

      -
    • -
    -
    -
    -
    -
    update_cgse ops --tag=2022.2.2+CGSE
    -
    -
    -
    -

    There should not be any changes to files in this location, otherwise an error will be shown and the process interrupted.

    -
    -
    -

    WHICH ERROR AND WHAT TO DO AFTERWARDS?

    -
    -
    -
    -
    -

    3. Starting the Core Services

    +

    2. Starting the Core Services

    The core services of the Common-EGSE are those services that should be running all the time and preferably on the dedicated EGSE server. In the production environment, i.e. when we are actually running the tests in the lab, these services will be started automatically when the system boots, see the section on the core services in systemd. During our development and when using the Common-EGSE outside of the Camera Test environment, we can start the core services on our local machine.

    @@ -1293,10 +1359,10 @@

    3. Starting the Core Services

    -

    4. The Graphical User Interfaces (GUI)

    +

    3. The Graphical User Interfaces (GUI)

    -

    4.1. Icons used for different GUIs

    +

    3.1. Icons used for different GUIs

    @@ -1413,7 +1479,7 @@

    4.1. Icons used for different GUIs

    -

    4.2. The Process Manager GUI

    +

    3.2. The Process Manager GUI

    This section describes the Process Manager GUI.

    @@ -1424,7 +1490,7 @@

    4.2. The Process Manager GUI

    -

    4.3. The CSL Operator GUI

    +

    3.3. The CSL Operator GUI

    This section briefly describes the CSL specific task GUI, the CSL Operator GUI.

    @@ -1437,20 +1503,20 @@

    4.3. The CSL Operator GUI

    -

    For more information on the working of the Task GUIs, please go and read the section on the Tasks GUI in Chapter 5.

    +

    For more information on the working of the Task GUIs, please go and read the section on the Tasks GUI in Chapter 4.

    -

    4.4. The Hexapod Puna GUI

    +

    3.4. The Hexapod Puna GUI

    -

    4.4.1. Monitoring and Commanding the Hexapod

    +

    3.4.1. Monitoring and Commanding the Hexapod

    Monitoring and commanding the hexapod can be done via a designated GUI. This is described in the sections below.

    -

    4.4.2. Synopsis

    +

    3.4.2. Synopsis

    To start the Hexapod Puna GUI, type the following command:

    @@ -1479,7 +1545,7 @@

    4.4.2. Synopsis

    -

    4.4.3. Description

    +

    3.4.3. Description

    A screenshot of the Hexapod Puna GUI is shown below.

    @@ -1511,7 +1577,7 @@

    4.4.3. Description

    -

    4.4.4. Toolbar

    +

    3.4.4. Toolbar

    Enable/Disable Amplifier
    @@ -1541,7 +1607,7 @@

    4.4.4. Toolbar

    -

    4.4.5. States

    +

    3.4.5. States

    The left panel reports on the status of the hexapod. This is done by means of a series of LEDS, where a green LED indicates information, an orange LED @@ -1553,7 +1619,7 @@

    4.4.5. States

    -

    4.4.6. Positions

    +

    3.4.6. Positions

    The middle panel displays the user and machine positions, and actuator lengths.

    @@ -1586,7 +1652,7 @@

    4.4.6. Positions

    -

    4.4.7. Tabs

    +

    3.4.7. Tabs

    The tabs in the right panel allow settings, movements, maintenance, etc. The different tabs are discussed in the subsequent sections.

    @@ -1642,11 +1708,11 @@

    4.4.7. Tabs

    -

    4.5. The HUBER Stages GUI

    +

    3.5. The HUBER Stages GUI

    -

    4.6. The DPU GUI

    +

    3.6. The DPU GUI

    The DPU GUI is a (near) real-time viewer for the data that is received from the N-FEE. I say near real-time because the data comes with a small delay due to the nature of the interface and the inner working of the N-FEE. Image data e.g. can take up to four seconds to transfer from the N-FEE to the DPU Processor just for one frame of one of the four CCDs. The housekeeping data is only sent once on every (or right after) a synchronisation pulse, which takes 6.25s for external synchronisation measurements. Because of this delay, you need to be aware of the current operating mode and the synchronisation mode of your observation to estimate how long it will take before you can inspect the data. Sometimes its quick like with internal sync measurements of a partial readout and a period of a second versus full frame external sync measurements. Since mode changes only happen on the long (400ms) pulses, it can take up to 25s before you can see the change in the GUI.

    @@ -1656,19 +1722,19 @@

    4.6. The DPU GUI

    To start up the DPU GUI, at least the core services need to be running on the server. The DPU GUI gets its information from the DPU Processor, so as long as this process is not running, you will see a CONNECTION LOST message as the N-FEE mode in the 'Mode Parameters' panel. As soon as the DPU Processor starts and connects to the N-FEE, the DPU GUI will receive data and update its displays. Whenever the DPU Processor loses connection with the N-FEE (e.g. because the camera was switched off) or the DPU Processor is terminated or crashed, the N-FEE Mode will show the CONNECTION LOST message again.

    -
    -
    +
    +
    +
      -
    • +
    • CONNECTION LOST

    • -
    • +
    • ON MODE

    -
    -
    +
    dpu ui started @@ -1676,7 +1742,7 @@

    4.6. The DPU GUI

    Figure 1. The DPU GUI when it is just been launched. There are no images for the CCDs, readout parameters have a default value and the N-FEE mode says 'CONNECTION LOST'. There is no connection with either the DPU Processor or the N-FEE.
    -
    +
    dpu ui started on mode @@ -1966,7 +2032,7 @@

    4.6. The DPU GUI

    -

    5. The Tasks GUI

    +

    4. The Tasks GUI

    The Tasks GUI is a collective noun for all the task GUIs that we use in our CGSE and TS environment. All these GUIs have the same principle user interface since they are all based on the same Python package that generates the Graphical interface and executes the code. That package is gui-executor which is under development at the institute of astronomy at KU Leuven. The gui-executor package is open-source and can be installed from PyPI with pip:

    @@ -2015,7 +2081,7 @@

    5. The Tasks GUI

    We will explain all of these panels in more detail next.

    -

    5.1. The Toolbar

    +

    4.1. The Toolbar

    The toolbar is dedicated to the Python kernel that is used to execute the tasks. The left-most button can be used to restart the kernel. Do this when you want to start a fresh new Python Interpreter or when you need to change the kernel. The second button on the toolbar is used to open a Python Console that is connected to the currently running kernel. Pressing this button will open a window with a prompt where you can enter or paste Python code to be executed. Here you can also find back the code that was executed by pressing one of the buttons.

    @@ -2036,32 +2102,32 @@

    5.1. The Toolbar

    -

    5.2. The Button Panel

    +

    4.2. The Button Panel

    All tasks are available in the Button Panel. The tasks are arranged in groups and in each group in columns of four tasks.

    -

    5.3. The Arguments Panel

    +

    4.3. The Arguments Panel

    When you press a task button an associated arguments panel will appear below the button panel. Before pressing the Run button you can provide input for all the parameters of the task. Most of the arguments will have a simple builtin type like int, float or bool, but more complex argument types are possible and for some of those a special icon will appear on the right side to

    -

    5.4. The Output Console

    +

    4.4. The Output Console

    TBW

    -

    5.5. Execute a Task

    +

    4.5. Execute a Task

    XXXXX: update text below! As an example, when you press a task button, it will change color to indicate this task has been selected (see screenshot above) and an arguments panel will appear in the middle of the GUI. The set_trp1 task expects one parameter (temperature) for which no default was provided. The expected input is a float. When you press the Run button, the task will be executed in the kernel. All tasks are by default executed in the kernel. You will sometimes see that a task will execute in the GUI App or as a script, don’t use those options yourself unless you know what you are doing.

    -

    5.6. Execute Tasks from the Jupyter QtConsole [aka REPL]

    +

    4.6. Execute Tasks from the Jupyter QtConsole [aka REPL]

    You might want to run tasks from the Jupyter QtConsole or any other REPL that you use. Since the tasks in the GUI as just like any other function, you can import the task and run the function from the Python interpreter. The only thing you need to know is where the tasks are defined, from which module they should be imported. In our PLATO project, all tasks are defined in the plato-test-scripts repository and they live inside the camtest.tasks package. It depends on the TAB and the location in that TAB where your tasks is defined. As an illustration, we start from the CSL Operator GUI (screenshot below) and select the Camera TAB and the Camera Switch ON button. This will open the arguments panel, and we see this task takes one argument, the hk_frequency. We would like to execute this task in the REPL instead of pressing the 'Run' button.

    @@ -2232,10 +2298,10 @@

    5.6. Execute Tasks from the Jupyter Qt

    -

    6. Frequently Asked Questions (FAQ)

    +

    5. Frequently Asked Questions (FAQ)

    -

    6.1. How can I check the installed version of the CGSE and Test Scripts

    +

    5.1. How can I check the installed version of the CGSE and Test Scripts

    Use the following commands in a terminal:

    @@ -2263,11 +2329,11 @@

    6.1

    -

    See the section Chapter 2 on how to get the latest version installed in your environment.

    +

    See the section [Update the Common-EGSE Software] on how to get the latest version installed in your environment.

    -

    6.2. How do I check if all the devices have been connected properly and are active?

    +

    5.2. How do I check if all the devices have been connected properly and are active?

    TBW

    @@ -2276,7 +2342,7 @@

    -

    6.3. How do I check if the Storage Manager is running?

    +

    5.3. How do I check if the Storage Manager is running?

    You can check this in a terminal with the following command:

    @@ -2324,7 +2390,7 @@

    6.3. How do I check i

    -

    6.4. How do I check if the Configuration Manager is running?

    +

    5.4. How do I check if the Configuration Manager is running?

    You can check this in a terminal with the following command:

    @@ -2344,7 +2410,7 @@

    6.4. How do I c

    -

    6.5. How do I check if the Process Manager is running?

    +

    5.5. How do I check if the Process Manager is running?

    You can check this in a terminal with the following command:

    @@ -2361,7 +2427,7 @@

    6.5. How do I check i

    -

    6.6. How do I check if the Synoptics Manager is running?

    +

    5.6. How do I check if the Synoptics Manager is running?

    You can check this in a terminal with the following command:

    @@ -2378,7 +2444,7 @@

    6.6. How do I check

    -

    6.7. How do I check if the Logger is running?

    +

    5.7. How do I check if the Logger is running?

    You can check this in a terminal with the following command:

    @@ -2389,7 +2455,7 @@

    6.7. How do I check if the Log

    -

    6.8. Where can I find my test data?

    +

    5.8. Where can I find my test data?

    TBW

    @@ -2412,54 +2478,135 @@

    6.8. Where can I find my test data?

    diff --git a/docs/categories/index.html b/docs/categories/index.html index 0d896d5..278da6c 100644 --- a/docs/categories/index.html +++ b/docs/categories/index.html @@ -7,7 +7,7 @@ The Common-EGSE Documentation - + @@ -38,9 +38,9 @@ + - - + @@ -180,7 +180,7 @@