QMainWindow(parent: QWidget = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())
Expand source code
-def is_avoidance_ok(hexusr,hexobj,setup=None,verbose=False):
+def is_avoidance_ok(hexusr, hexobj, setup: Setup = None, verbose=False):
"""
is_avoidance_ok(hexusr,hexobj,setup=None)
@@ -178,11 +178,11 @@
if not provided, GlobalState.setup is used
- OUTPUT : Boolean indicating wheather or not the FPA is outside of the avoidance volume around L6
+ OUTPUT : Boolean indicating whether the FPA is outside the avoidance volume around L6
"""
if setup is None:
- setup = GlobalState.setup
+ setup = load_setup()
"""
A. HORIZONTAL AVOIDANCE
@@ -196,10 +196,10 @@
# l6xy = the projection of the origin of HEX_USR on the X-Y plane of FPA_SEN
l6xy = hexusr.getOrigin().expressIn(hexobj)[:2]
- # !! This is a verification of the current situation --> need to replace by a simulation of the forthcoming movement in the building block
+ # !! This is a verification of the current situation --> need to replace by a simulation of the forthcoming
+ # movement in the building block
horizontal_check = ((l6xy[0]**2.+l6xy[1]**2.) < clearance_xy*clearance_xy)
-
-
+
"""
B. VERTICAL AVOIDANCE
Ensure that the CCD never hits L6.
@@ -212,26 +212,25 @@
clearance_z = setup.camera.fpa.avoidance.clearance_z
# Vertices = Points representing the vertices of the avoidance volume above the FPA (60)
- vertices_nb = setup.camera.fpa.avoidance.vertices_nb
+ vertices_nb = setup.camera.fpa.avoidance.vertices_nb
# All vertices are on a circle of radius 'vertices_radius' (100 mm)
vertices_radius = setup.camera.fpa.avoidance.vertices_radius
- angles = np.linspace(0,np.pi * 2,vertices_nb,endpoint=False)
+ angles = np.linspace(0, np.pi * 2, vertices_nb, endpoint=False)
vertices_x = np.cos(angles) * vertices_radius
vertices_y = np.sin(angles) * vertices_radius
vertices_z = np.ones_like(angles) * clearance_z
# The collection of Points defining the avoidance volume around FPA_SEN
- vert_obj = Points(coordinates=np.array([vertices_x,vertices_y,vertices_z]),ref=hexobj,name="vert_obj")
+ vert_obj = Points(coordinates=np.array([vertices_x, vertices_y, vertices_z]), ref=hexobj, name="vert_obj")
# Their coordinates in HEX_USR
# NB: vert_obj is a Points, vert_usr is an array
vert_usr = vert_obj.expressIn(hexusr)
-
-
+
# !! Same as above : this is verifying the current situation, not the one after a planned movement
# Verify that all vertices ("protecting" FPA_SEN) are below the x-y plane of HEX_USR ("protecting" L6)
- vertical_check = (np.all(vert_usr[2,:] < 0.))
+ vertical_check = (np.all(vert_usr[2, :] < 0.))
if verbose:
printdict = {True:"OK", False:"NOT OK"}
@@ -243,8 +242,8 @@
coobj = vert_obj.coordinates
for i in range(vertices_nb):
print(f"{i} OBJ {np.round(coobj[:3,i],6)} --> USR {np.round(vert_usr[:3,i],6)}")
- vert_z = vert_usr[2,:]
- vert_zi = np.where(vert_z==np.max(vert_z))
+ vert_z = vert_usr[2, :]
+ vert_zi = np.where(vert_z == np.max(vert_z))
print(f"#vertices at max z : {len(vert_zi[0])}")
print(f"First one: vertex {vert_zi[0][0]} : {np.round(vert_usr[:3,vert_zi[0][0]],6)}")
diff --git a/docs/api/egse/coordinates/cslmodel.html b/docs/api/egse/coordinates/cslmodel.html
index 0793757..f8d3303 100644
--- a/docs/api/egse/coordinates/cslmodel.html
+++ b/docs/api/egse/coordinates/cslmodel.html
@@ -54,7 +54,7 @@ Module egse.coordinates.cslmodel
class CSLReferenceFrameModel(ReferenceFrameModel):
"""
The CSL Reference Frame Model is a specific reference model that adds convenience methods
- for manipulating the Hexapod PUNA which is part of the overall CSH Setup.
+ for manipulating the Hexapod PUNA which is part of the overall CSL Setup.
"""
_DEGREES_DEFAULT = ReferenceFrameModel._DEGREES_DEFAULT
@@ -175,7 +175,7 @@
The CSL Reference Frame Model is a specific reference model that adds convenience methods
-for manipulating the Hexapod PUNA which is part of the overall CSH Setup.
+for manipulating the Hexapod PUNA which is part of the overall CSL Setup.
When the model_dict is empty or None, a new model is created with a master reference frame.
Args
@@ -192,7 +192,7 @@ Args
class CSLReferenceFrameModel(ReferenceFrameModel):
"""
The CSL Reference Frame Model is a specific reference model that adds convenience methods
- for manipulating the Hexapod PUNA which is part of the overall CSH Setup.
+ for manipulating the Hexapod PUNA which is part of the overall CSL Setup.
"""
_DEGREES_DEFAULT = ReferenceFrameModel._DEGREES_DEFAULT
diff --git a/docs/api/egse/coordinates/index.html b/docs/api/egse/coordinates/index.html
index 677e252..2e90989 100644
--- a/docs/api/egse/coordinates/index.html
+++ b/docs/api/egse/coordinates/index.html
@@ -49,7 +49,7 @@ Module egse.coordinates
from egse.coordinates.referenceFrame import ReferenceFrame
from egse.settings import Settings
from egse.state import GlobalState
-from egse.setup import NavigableDict
+from egse.setup import NavigableDict, load_setup, Setup
logger = logging.getLogger(__name__)
@@ -175,19 +175,22 @@ Module egse.coordinates
return x_undistorted, y_undistorted
-def focal_plane_to_ccd_coordinates(x_fp, y_fp):
+def focal_plane_to_ccd_coordinates(x_fp, y_fp, setup: Setup = None):
"""
Conversion from focal-plane to pixel coordinates on the appropriate CCD.
Args:
x_fp: Focal-plane x-coordinate [mm].
y_fp: Focal-plane y-coordinate [mm].
+ setup: Setup
Returns:
Pixel coordinates (row, column) and the corresponding CCD. If the given
focal-plane coordinates do not fall on any CCD, (None, None, None) is
returned.
"""
- setup = GlobalState.setup
+
+ if setup is None:
+ setup = load_setup()
if setup is not None:
num_rows = setup.camera.ccd.num_rows
@@ -1044,7 +1047,7 @@ Returns
-def focal_plane_to_ccd_coordinates (x_fp, y_fp)
+def focal_plane_to_ccd_coordinates (x_fp, y_fp, setup: Setup = None)
Conversion from focal-plane to pixel coordinates on the appropriate CCD.
@@ -1054,6 +1057,8 @@
Args
Focal-plane x-coordinate [mm].
y_fp
Focal-plane y-coordinate [mm].
+
setup
+
Setup
Returns
Pixel coordinates (row, column) and the corresponding CCD.
@@ -1064,19 +1069,22 @@
Returns
Expand source code
-
def focal_plane_to_ccd_coordinates(x_fp, y_fp):
+def focal_plane_to_ccd_coordinates(x_fp, y_fp, setup: Setup = None):
"""
Conversion from focal-plane to pixel coordinates on the appropriate CCD.
Args:
x_fp: Focal-plane x-coordinate [mm].
y_fp: Focal-plane y-coordinate [mm].
+ setup: Setup
Returns:
Pixel coordinates (row, column) and the corresponding CCD. If the given
focal-plane coordinates do not fall on any CCD, (None, None, None) is
returned.
"""
- setup = GlobalState.setup
+
+ if setup is None:
+ setup = load_setup()
if setup is not None:
num_rows = setup.camera.ccd.num_rows
diff --git a/docs/api/egse/coordinates/laser_tracker_to_dict.html b/docs/api/egse/coordinates/laser_tracker_to_dict.html
index 6d340e5..8e445dc 100644
--- a/docs/api/egse/coordinates/laser_tracker_to_dict.html
+++ b/docs/api/egse/coordinates/laser_tracker_to_dict.html
@@ -41,8 +41,10 @@ Module egse.coordinates.laser_tracker_to_dict
Module egse.coordinates.laser_tracker_to_dict
Module egse.coordinates.laser_tracker_to_dict
Module egse.coordinates.laser_tracker_to_dict
Module egse.coordinates.laser_tracker_to_dict
Module egse.coordinates.laser_tracker_to_dict
Functions
-def laser_tracker_to_dict (filexls, setup)
+def laser_tracker_to_dict (filexls, setup: Setup )
laser_tracker_to_dict(filexls)
@@ -173,7 +175,7 @@
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 @@
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 @@
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 @@
("Master" is an exception)
"""
-
+
# Predefined model -- gliso ~ master
-
+
"""
predef_refs={}
predef_refs['gltab'] = 'glfix'
@@ -239,7 +241,7 @@
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 @@
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
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
+
+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 @@
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
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 @@
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
-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 @@
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 @@
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 @@
-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 @@
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 @@
-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 @@
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
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 @@
+
+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)
+
+
+
+
+
+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 @@
@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 @@
# 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 @@
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 @@
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
+
+
+
+
quit
@@ -1388,10 +2155,15 @@
@@ -1417,11 +2189,25 @@
+
+
+
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
-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 @@
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 @@
location = get_data_storage_location()
- previous_obsid = None
+ syn_obsid = None
while self.keep_processing_queue:
@@ -4162,6 +4202,7 @@
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 @@
# 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 @@
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_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 @@
( 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
-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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
# 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
-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 @@
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 @@
-
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 @@
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
+
+def esl_get_active_link (esl_link)
+
+
+
+
+
+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()
+
+
+
+def esl_get_number_of_links (esl_link)
+
+
+
+
+
+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)
+
+
+
+def esl_is_link_connected (esl_link)
+
+
+
+
+
+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
+
+def esl_set_active_link (esl_link, active_link)
+
+
+
+
+
+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
-
-class ESL
-( *args, **kwargs)
-
-
-
-
-
-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
-
-
-
-var auto_flush
-
-
-
-var check_record_writes
-
-
-
-var cmd_buffer
-
-
-
-var context
-
-
-
-var current_port
-
-
-
-var eintr_enabled
-
-
-
-var epollfd
-
-
-
-var extension_cb
-
-
-
-var extn_byte
-
-
-
-var extn_cmd
-
-
-
-var extn_count
-
-
-
-var extn_link
-
-
-
-var extn_size
-
-
-
-var file
-
-
-
-var filesize
-
-
-
-var live
-
-
-
-var log_file
-
-
-
-var log_rx_port
-
-
-
-var log_tx_port
-
-
-
-var max_dumps
-
-
-
-var max_length
-
-
-
-var number_of_links
-
-
-
-var number_of_slots
-
-
-
-var option
-
-
-
-var options
-
-
-
-var partialframe_offset
-
-
-
-var partialframesz
-
-
-
-var ram_io
-
-
-
-var ram_io_int
-
-
-
-var read_calls
-
-
-
-var read_io_calls
-
-
-
-var record_file
-
-
-
-var recordsize
-
-
-
-var rx_buffer
-
-
-
-var rx_buffer_content
-
-
-
-var rx_buffer_length
-
-
-
-var rx_buffer_ptr
-
-
-
-var rx_count
-
-
-
-var rx_final_flags
-
-
-
-var rx_param
-
-
-
-var rx_state
-
-
-
-var rx_timeout
-
-
-
-var rx_timeout_returns_error
-
-
-
-var rxinfo
-
-
-
-var slot_content
-
-
-
-var sock
-
-
-
-var special_cb
-
-
-
-var system_type
-
-
-
-var time_buffer
-
-
-
-var timedata
-
-
-
-var timedata_ns
-
-
-
-var timezero
-
-
-
-var total_raw_bytes_received
-
-
-
-var tx_buffer
-
-
-
-var tx_buffer_content
-
-
-
-var tx_buffer_length
-
-
-
-
-
class ESLError
( *args, **kwargs)
@@ -2292,18 +1762,6 @@ Ancestors
builtins.BaseException
-
-class esl_p
-( *args, **kwargs)
-
-
-
-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
+
@@ -38,7 +76,7 @@
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
-
-
-
-
-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
+
-
-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)
-
-
+
+
-
-def reply_address_length (instruction)
+
+def rmap_close_connection (rmap_link)
-Returns the content of the replay address length field.
-
The size of the replay address field is then decoded from the following table:
-
Address Field Length | Size of Address Field
-----------------------+-----------------------
- 0b00 | 0 bytes
- 0b01 | 4 bytes
- 0b10 | 8 bytes
- 0b11 | 12 bytes
-
+
Expand source code
-def reply_address_length(instruction):
- """Returns the content of the replay address length field.
-
- The size of the replay address field is then decoded from the following table:
-
- Address Field Length | Size of Address Field
- ----------------------+-----------------------
- 0b00 | 0 bytes
- 0b01 | 4 bytes
- 0b10 | 8 bytes
- 0b11 | 12 bytes
-
- """
- return (instruction & 0b00000011) << 2
+def rmap_close_connection(rmap_link):
+ LOGGER.info("RMAP connection closed successfully.")
@@ -2155,10 +819,11 @@ Returns
Nothing
"""
- rmap_set_initiator_logical_address(rmap_link, initiator_logical_address)
- rmap_set_target_key(rmap_link, target_key)
- rmap_set_target_logical_address(rmap_link, target_logical_address)
- rmap_set_target_spw_address(rmap_link, b'\x00', 0)
+ rmap_link.initiator_logical_address = initiator_logical_address
+ rmap_link.target_key = target_key
+ rmap_link.target_logical_address = target_logical_address
+ rmap_link.target_spw_address = 0x0
+ rmap_link.target_spw_address_len = 0
@@ -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
-
-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)
-
+
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
-
+
var initiator_logical_address
-
+
var reply_spw_address
-
+
var reply_spw_address_len
-
+
var spw_device
-
+
var target_key
-
+
var target_logical_address
-
+
var target_spw_address
-
+
var target_spw_address_len
-
+
var transaction_identifier
-
+
-
-class rmap_p
-( *args, **kwargs)
-
-
-
-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
-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 @@
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 @@
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 @@
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 @@
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 @@
# 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)
+
+def get_active_link (self)
-
+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
+
+
+
+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.
+
+
+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
diff --git a/docs/api/egse/dsi/virtual_dsi.html b/docs/api/egse/dsi/virtual_dsi.html
new file mode 100644
index 0000000..ed73a17
--- /dev/null
+++ b/docs/api/egse/dsi/virtual_dsi.html
@@ -0,0 +1,544 @@
+
+
+
+
+
+
+egse.dsi.virtual_dsi API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Module egse.dsi.virtual_dsi
+
+
+
+
+Expand source code
+
+import logging
+import selectors
+import socket
+import types
+
+from egse.dsi import constants
+from egse.dsi.esl import esl_configure
+from egse.dsi.esl import esl_connection
+from egse.dsi.esl import esl_get_number_of_links
+from egse.dsi.esl import esl_is_link_connected
+from egse.dsi.esl import esl_print_info
+from egse.dsi.esl import esl_set_active_link
+from egse.dsi.esl import esl_get_active_link
+from egse.dsi.esl import esl_set_mode
+from egse.dsi.esl import esl_set_speed
+from egse.dsi.rmap import rmap_connection
+from egse.dsi.spw import SpaceWireOverDSI
+from egse.logger import set_all_logger_levels
+from egse.spw import SpaceWirePacket
+
+_LOGGER = logging.getLogger('egse.virtual_dsi')
+
+set_all_logger_levels(logging.DEBUG)
+
+IP_ADDRESS_DSI = "192.168.0.17"
+V_DSI_HOST = "localhost"
+V_DSI_PORT = 59360
+
+selector = selectors.DefaultSelector()
+
+
+class Message:
+ def __init__(self, selector, sock, addr):
+ self.selector = selector
+ self.sock = sock
+ self.addr = addr
+ self.raddr = self._get_remote_addr()
+
+ def _get_remote_addr(self) -> tuple:
+ return self.sock.getpeername()
+
+ def __str__(self):
+ msg = (
+ f"Remote address: {self.raddr}"
+ )
+ return msg
+
+ def process_events(self, mask):
+ if mask & selectors.EVENT_READ:
+ self.read()
+ if mask & selectors.EVENT_WRITE:
+ self.write()
+
+ def read(self):
+ pass
+
+ def write(self):
+ pass
+
+
+def accept_wrapper(sock: socket.socket):
+ conn, addr = sock.accept()
+ _LOGGER.debug(f"{conn = }, {type(conn) = }")
+ _LOGGER.info(f"Accepted connection from {addr}")
+ conn.setblocking(False)
+ message = Message(selector, conn, addr)
+ events = selectors.EVENT_READ | selectors.EVENT_WRITE
+ selector.register(conn, events, data=message)
+
+
+def service_connection(key, mask):
+ """
+ Handle read and write events for the service application, i.e. feesim and dpu_cs.
+ """
+ sock = key.fileobj
+ data = key.data
+ if mask & selectors.EVENT_READ:
+ try:
+ recv_data = sock.recv(1024) # Should be ready to read
+ except ConnectionResetError as exc:
+ _LOGGER.warning(f"{sock} closed by peer")
+ recv_data = None
+
+ if recv_data:
+ _LOGGER.debug(f"{recv_data = }, {SpaceWirePacket.create_packet(recv_data)}")
+ else:
+ _LOGGER.info(f"Closing connection to {data.addr}")
+ selector.unregister(sock)
+ sock.close()
+ if mask & selectors.EVENT_WRITE:
+ if data.outb:
+ # This data should be put on the right SpW port. How do we know which port to use?
+ _LOGGER.debug(f"Echoing {data.outb!r} to {data.addr}")
+ # sent = sock.send(data.outb) # Should be ready to write
+ # data.outb = data.outb[sent:]
+
+
+def service_dsi(transport, key, mask):
+ sock = key.fileobj
+ data = key.data
+
+ # Need to keep state of the active port, we can get the current port from transport
+ # transport.get_active_link(). How do we determine from which port a packet is read?
+ # Is it read from the currently active port? Can we then assume it is sent from app_num
+ # 1 or 2 (feesim or dpu_cs)? We need this information in order to forward the packet
+ # to the other app.
+
+ # print(f"{mask = }, {key.fileobj = }")
+
+ if mask & selectors.EVENT_READ:
+ _LOGGER.info(f"Reading data from DSI")
+ try:
+ _, data = transport.read_packet()
+ dsi_port = transport.get_active_link()
+ except ConnectionResetError as exc:
+ _LOGGER.warning(f"{sock} closed by peer")
+ data = None
+
+ if data:
+ _LOGGER.info(f"DSI {data = }, packet = {SpaceWirePacket.create_packet(data)}")
+ else:
+ _LOGGER.info(f"Closing connection to DSI")
+ selector.unregister(sock)
+ sock.close()
+
+ if mask & selectors.EVENT_WRITE:
+ # Socket is always writable, so we need a check here if there is data to write
+ if data:
+ _LOGGER.info("Writing data to DSI")
+
+
+def main():
+ dsi_address = IP_ADDRESS_DSI
+ dsi_port = 1
+ # dsi_address = "127.0.0.1" # this will raise a egse.dsi.esl.ESLError in esl_connection()
+
+ server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ _LOGGER.info(f"{server_sock = }")
+ # Avoid bind() exception: OSError: [Errno 48] Address already in use
+ server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ server_sock.bind((V_DSI_HOST, V_DSI_PORT))
+ server_sock.listen()
+ server_sock.setblocking(False)
+ selector.register(server_sock, selectors.EVENT_READ, data=None)
+
+ with esl_connection(dsi_address) as esl_link, rmap_connection(esl_link) as rmap_link:
+
+ transport = SpaceWireOverDSI(dsi_address, dsi_port, esl_link, rmap_link)
+
+ esl_configure(esl_link)
+ esl_print_info(esl_link)
+
+ status = esl_set_speed(esl_link, 100)
+ status = esl_set_mode(esl_link, constants.ESL_LINK_MODE_NORMAL)
+
+ sock_dsi = transport.get_socket()
+ selector.register(sock_dsi, selectors.EVENT_READ | selectors.EVENT_WRITE, data=None)
+
+ _LOGGER.info(f"{sock_dsi = }")
+ _LOGGER.info(f"{transport.get_socket() = }")
+ _LOGGER.info(f"{esl_get_number_of_links(esl_link) = }")
+ _LOGGER.info(f"{esl_link.contents.current_port = }")
+ _LOGGER.info(f"{esl_get_active_link(esl_link) = }")
+ _LOGGER.info(f"{esl_is_link_connected(esl_link) = }")
+ _LOGGER.info(f"{esl_set_active_link(esl_link, 2) = }")
+ _LOGGER.info(f"{esl_link.contents.current_port = }")
+ _LOGGER.info(f"{esl_get_active_link(esl_link) = }")
+ _LOGGER.info(f"{esl_is_link_connected(esl_link) = }")
+
+
+ try:
+ while True:
+ events = selector.select(timeout=None)
+ # print(f"{events = }")
+ for key, mask in events:
+ # print(f"{key = }, {mask = }")
+
+ if key.fileobj is sock_dsi:
+ service_dsi(transport, key, mask)
+ continue
+
+ if key.data is None:
+ # The listener for incoming connections on V_DSI_PORT,
+ # you will need to accept this request to connect.
+ accept_wrapper(key.fileobj)
+ else:
+ # Event is from a client socket, handle it
+ service_connection(key, mask)
+ except KeyboardInterrupt:
+ print("Caught keyboard interrupt, exiting")
+ finally:
+ selector.close()
+
+ # Disconnect applications
+
+ _LOGGER.info(f"Closing connection {V_DSI_HOST}:{V_DSI_PORT}")
+ server_sock.close()
+
+
+if __name__ == '__main__':
+ main()
+
+
+
+
+
+
+
+
+def accept_wrapper (sock: socket.socket)
+
+
+
+
+
+Expand source code
+
+def accept_wrapper(sock: socket.socket):
+ conn, addr = sock.accept()
+ _LOGGER.debug(f"{conn = }, {type(conn) = }")
+ _LOGGER.info(f"Accepted connection from {addr}")
+ conn.setblocking(False)
+ message = Message(selector, conn, addr)
+ events = selectors.EVENT_READ | selectors.EVENT_WRITE
+ selector.register(conn, events, data=message)
+
+
+
+def main ()
+
+
+
+
+
+Expand source code
+
+def main():
+ dsi_address = IP_ADDRESS_DSI
+ dsi_port = 1
+ # dsi_address = "127.0.0.1" # this will raise a egse.dsi.esl.ESLError in esl_connection()
+
+ server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ _LOGGER.info(f"{server_sock = }")
+ # Avoid bind() exception: OSError: [Errno 48] Address already in use
+ server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ server_sock.bind((V_DSI_HOST, V_DSI_PORT))
+ server_sock.listen()
+ server_sock.setblocking(False)
+ selector.register(server_sock, selectors.EVENT_READ, data=None)
+
+ with esl_connection(dsi_address) as esl_link, rmap_connection(esl_link) as rmap_link:
+
+ transport = SpaceWireOverDSI(dsi_address, dsi_port, esl_link, rmap_link)
+
+ esl_configure(esl_link)
+ esl_print_info(esl_link)
+
+ status = esl_set_speed(esl_link, 100)
+ status = esl_set_mode(esl_link, constants.ESL_LINK_MODE_NORMAL)
+
+ sock_dsi = transport.get_socket()
+ selector.register(sock_dsi, selectors.EVENT_READ | selectors.EVENT_WRITE, data=None)
+
+ _LOGGER.info(f"{sock_dsi = }")
+ _LOGGER.info(f"{transport.get_socket() = }")
+ _LOGGER.info(f"{esl_get_number_of_links(esl_link) = }")
+ _LOGGER.info(f"{esl_link.contents.current_port = }")
+ _LOGGER.info(f"{esl_get_active_link(esl_link) = }")
+ _LOGGER.info(f"{esl_is_link_connected(esl_link) = }")
+ _LOGGER.info(f"{esl_set_active_link(esl_link, 2) = }")
+ _LOGGER.info(f"{esl_link.contents.current_port = }")
+ _LOGGER.info(f"{esl_get_active_link(esl_link) = }")
+ _LOGGER.info(f"{esl_is_link_connected(esl_link) = }")
+
+
+ try:
+ while True:
+ events = selector.select(timeout=None)
+ # print(f"{events = }")
+ for key, mask in events:
+ # print(f"{key = }, {mask = }")
+
+ if key.fileobj is sock_dsi:
+ service_dsi(transport, key, mask)
+ continue
+
+ if key.data is None:
+ # The listener for incoming connections on V_DSI_PORT,
+ # you will need to accept this request to connect.
+ accept_wrapper(key.fileobj)
+ else:
+ # Event is from a client socket, handle it
+ service_connection(key, mask)
+ except KeyboardInterrupt:
+ print("Caught keyboard interrupt, exiting")
+ finally:
+ selector.close()
+
+ # Disconnect applications
+
+ _LOGGER.info(f"Closing connection {V_DSI_HOST}:{V_DSI_PORT}")
+ server_sock.close()
+
+
+
+def service_connection (key, mask)
+
+
+Handle read and write events for the service application, i.e. feesim and dpu_cs.
+
+
+Expand source code
+
+def service_connection(key, mask):
+ """
+ Handle read and write events for the service application, i.e. feesim and dpu_cs.
+ """
+ sock = key.fileobj
+ data = key.data
+ if mask & selectors.EVENT_READ:
+ try:
+ recv_data = sock.recv(1024) # Should be ready to read
+ except ConnectionResetError as exc:
+ _LOGGER.warning(f"{sock} closed by peer")
+ recv_data = None
+
+ if recv_data:
+ _LOGGER.debug(f"{recv_data = }, {SpaceWirePacket.create_packet(recv_data)}")
+ else:
+ _LOGGER.info(f"Closing connection to {data.addr}")
+ selector.unregister(sock)
+ sock.close()
+ if mask & selectors.EVENT_WRITE:
+ if data.outb:
+ # This data should be put on the right SpW port. How do we know which port to use?
+ _LOGGER.debug(f"Echoing {data.outb!r} to {data.addr}")
+ # sent = sock.send(data.outb) # Should be ready to write
+ # data.outb = data.outb[sent:]
+
+
+
+def service_dsi (transport, key, mask)
+
+
+
+
+
+Expand source code
+
+def service_dsi(transport, key, mask):
+ sock = key.fileobj
+ data = key.data
+
+ # Need to keep state of the active port, we can get the current port from transport
+ # transport.get_active_link(). How do we determine from which port a packet is read?
+ # Is it read from the currently active port? Can we then assume it is sent from app_num
+ # 1 or 2 (feesim or dpu_cs)? We need this information in order to forward the packet
+ # to the other app.
+
+ # print(f"{mask = }, {key.fileobj = }")
+
+ if mask & selectors.EVENT_READ:
+ _LOGGER.info(f"Reading data from DSI")
+ try:
+ _, data = transport.read_packet()
+ dsi_port = transport.get_active_link()
+ except ConnectionResetError as exc:
+ _LOGGER.warning(f"{sock} closed by peer")
+ data = None
+
+ if data:
+ _LOGGER.info(f"DSI {data = }, packet = {SpaceWirePacket.create_packet(data)}")
+ else:
+ _LOGGER.info(f"Closing connection to DSI")
+ selector.unregister(sock)
+ sock.close()
+
+ if mask & selectors.EVENT_WRITE:
+ # Socket is always writable, so we need a check here if there is data to write
+ if data:
+ _LOGGER.info("Writing data to DSI")
+
+
+
+
+
+
+
+
+class Message
+( selector, sock, addr)
+
+
+
+
+
+Expand source code
+
+class Message:
+ def __init__(self, selector, sock, addr):
+ self.selector = selector
+ self.sock = sock
+ self.addr = addr
+ self.raddr = self._get_remote_addr()
+
+ def _get_remote_addr(self) -> tuple:
+ return self.sock.getpeername()
+
+ def __str__(self):
+ msg = (
+ f"Remote address: {self.raddr}"
+ )
+ return msg
+
+ def process_events(self, mask):
+ if mask & selectors.EVENT_READ:
+ self.read()
+ if mask & selectors.EVENT_WRITE:
+ self.write()
+
+ def read(self):
+ pass
+
+ def write(self):
+ pass
+
+Methods
+
+
+def process_events (self, mask)
+
+
+
+
+
+Expand source code
+
+def process_events(self, mask):
+ if mask & selectors.EVENT_READ:
+ self.read()
+ if mask & selectors.EVENT_WRITE:
+ self.write()
+
+
+
+def read (self)
+
+
+
+
+
+Expand source code
+
+def read(self):
+ pass
+
+
+
+def write (self)
+
+
+
+
+
+Expand source code
+
+def write(self):
+ pass
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/api/egse/dummy.html b/docs/api/egse/dummy.html
index 4af3841..8de5650 100644
--- a/docs/api/egse/dummy.html
+++ b/docs/api/egse/dummy.html
@@ -31,14 +31,24 @@ Module egse.dummy
This module provides dummy implementation for classes of the Commanding chain.
"""
import logging
+import multiprocessing
import pickle
+import random
import sys
+import threading
+import time
+from functools import partial
import click
import zmq
from egse.command import ClientServerCommand
+from egse.confman import is_configuration_manager_active
from egse.control import ControlServer
+from egse.control import is_control_server_active
+from egse.decorators import dynamic_interface
+from egse.listener import Event
+from egse.listener import EventInterface
from egse.protocol import CommandProtocol
from egse.proxy import Proxy
from egse.settings import Settings
@@ -48,7 +58,7 @@ Module egse.dummy
from egse.zmq_ser import connect_address
logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL)
-logger = logging.getLogger("DUMMY")
+LOGGER = logging.getLogger("egse.dummy")
# Especially DummyCommand and DummyController need to be defined in a known module
# because those objects are pickled and when de-pickled at the clients side the class
@@ -63,7 +73,9 @@ Module egse.dummy
'COMMANDING_PORT': 4443,
'SERVICE_PORT': 4444,
'MONITORING_PORT': 4445,
- 'PROTOCOL': 'tcp'
+ 'PROTOCOL': 'tcp',
+ 'TIMEOUT': 10,
+ 'HK_DELAY': 1.0,
}
)
@@ -78,12 +90,36 @@ Module egse.dummy
'device_method': 'response',
'cmd': '{one} {two} {fake}',
'response': 'handle_device_method'
- }
+ },
+ 'handle_event': {
+ 'description': "Notification of an event",
+ 'device_method': 'handle_event',
+ 'cmd': '{event}',
+ 'response': 'handle_device_method'
+ },
}
)
+def is_dummy_cs_active():
+ return is_control_server_active(
+ endpoint=connect_address(ctrl_settings.PROTOCOL, ctrl_settings.HOSTNAME, ctrl_settings.COMMANDING_PORT)
+ )
+
+
+class DummyCommand(ClientServerCommand):
+ pass
+
+
+class DummyInterface:
+ @dynamic_interface
+ def info(self):
+ ...
+ @dynamic_interface
+ def response(self, *args, **kwargs):
+ ...
+
-class DummyProxy(Proxy):
+class DummyProxy(Proxy, DummyInterface, EventInterface):
def __init__(self,
protocol=ctrl_settings.PROTOCOL, hostname=ctrl_settings.HOSTNAME, port=ctrl_settings.COMMANDING_PORT):
"""
@@ -92,20 +128,46 @@ Module egse.dummy
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))
+ super().__init__(connect_address(protocol, hostname, port), timeout=ctrl_settings.TIMEOUT)
-class DummyCommand(ClientServerCommand):
- pass
-
+class DummyController(DummyInterface, EventInterface):
+ def __init__(self, control_server):
+ self._cs = control_server
-class DummyController:
def info(self):
return "method info() called on DummyController"
def response(self, *args, **kwargs):
return f"response({args}, {kwargs})"
+ def handle_event(self, event: Event) -> str:
+
+ _exec_in_thread = False
+
+ def _handle_event(event):
+ LOGGER.info(f"An event is received, {event=}")
+ LOGGER.info(f"CM CS active? {is_configuration_manager_active()}")
+ time.sleep(5.0)
+ LOGGER.info(f"CM CS active? {is_configuration_manager_active()}")
+ LOGGER.info(f"An event is processed, {event=}")
+
+ if _exec_in_thread:
+ # We execute this function in a daemon thread so the acknowledgment is
+ # sent back immediately (the ACK means 'command received and will be
+ # executed).
+
+ retry_thread = threading.Thread(target=_handle_event, args=(event,))
+ retry_thread.daemon = True
+ retry_thread.start()
+ else:
+ # An alternative to the daemon thread is to create a scheduled task that will be executed
+ # after the event is acknowledged.
+
+ self._cs.schedule_task(partial(_handle_event, event))
+
+ return "ACK"
+
class DummyProtocol(CommandProtocol):
@@ -113,12 +175,14 @@ Module egse.dummy
super().__init__()
self.control_server = control_server
- self.device_controller = DummyController()
+ self.device_controller = DummyController(control_server)
self.load_commands(commands, DummyCommand, DummyController)
self.build_device_method_lookup_table(self.device_controller)
+ self._count = 0
+
def get_bind_address(self):
return bind_address(self.control_server.get_communication_protocol(), self.control_server.get_commanding_port())
@@ -126,8 +190,22 @@ Module egse.dummy
return super().get_status()
def get_housekeeping(self) -> dict:
+
+ # LOGGER.debug(f"Executing get_housekeeping function for {self.__class__.__name__}.")
+
+ self._count += 1
+
+ # use the sleep to test the responsiveness of the control server when even this get_housekeeping function takes
+ # a lot of time, i.e. up to several minutes in the case of data acquisition devices
+ # import time
+ # time.sleep(2.0)
+
+
return {
'timestamp': format_datetime(),
+ 'COUNT': self._count,
+ 'PI': 3.14159, # just to have a constant parameter
+ 'Random': random.randint(0, 100), # just to have a variable parameter
}
@@ -146,6 +224,8 @@ Module egse.dummy
"""
def __init__(self):
+ multiprocessing.current_process().name = "dummy_cs"
+
super().__init__()
self.device_protocol = DummyProtocol(self)
@@ -156,6 +236,17 @@ Module egse.dummy
self.poller.register(self.dev_ctrl_cmd_sock, zmq.POLLIN)
+ self.set_hk_delay(ctrl_settings.HK_DELAY)
+
+ from egse.confman import ConfigurationManagerProxy
+ from egse.listener import EVENT_ID
+
+ self.register_as_listener(
+ proxy=ConfigurationManagerProxy,
+ listener={'name': 'Dummy CS', 'proxy': DummyProxy, 'event_id': EVENT_ID.SETUP}
+ )
+
+
def get_communication_protocol(self):
return 'tcp'
@@ -168,14 +259,27 @@ Module egse.dummy
def get_monitoring_port(self):
return ctrl_settings.MONITORING_PORT
+ def get_storage_mnemonic(self):
+ return "DUMMY-HK"
+
+ def after_serve(self):
+ from egse.confman import ConfigurationManagerProxy
+
+ self.unregister_as_listener(proxy=ConfigurationManagerProxy, listener={'name': 'Dummy CS'})
+
@click.group()
def cli():
pass
-
-@cli.command()
+@click.group()
def control_server():
+ pass
+
+cli.add_command(control_server)
+
+@control_server.command()
+def start():
"""Start the dummy control server on localhost."""
from egse.dummy import DummyControlServer
@@ -191,6 +295,13 @@ Module egse.dummy
import traceback
traceback.print_exc(file=sys.stdout)
+@control_server.command()
+def stop():
+ """Send a quit service command to the dummy control server."""
+ with DummyProxy() as dummy:
+ sp = dummy.get_service_proxy()
+ sp.quit_server()
+
@cli.command()
@click.argument('hostname')
@@ -226,9 +337,9 @@ Module egse.dummy
if use_pickle:
response = pickle.loads(response)
- logger.info(response)
+ LOGGER.info(response)
except KeyboardInterrupt:
- logger.info("KeyboardInterrupt caught!")
+ LOGGER.info("KeyboardInterrupt caught!")
break
@@ -241,6 +352,24 @@ Module egse.dummy
+
+
+
+def is_dummy_cs_active ()
+
+
+
+
+
+Expand source code
+
+def is_dummy_cs_active():
+ return is_control_server_active(
+ endpoint=connect_address(ctrl_settings.PROTOCOL, ctrl_settings.HOSTNAME, ctrl_settings.COMMANDING_PORT)
+ )
+
+
+
@@ -313,6 +442,8 @@ Inherited members
"""
def __init__(self):
+ multiprocessing.current_process().name = "dummy_cs"
+
super().__init__()
self.device_protocol = DummyProtocol(self)
@@ -323,6 +454,17 @@ Inherited members
self.poller.register(self.dev_ctrl_cmd_sock, zmq.POLLIN)
+ self.set_hk_delay(ctrl_settings.HK_DELAY)
+
+ from egse.confman import ConfigurationManagerProxy
+ from egse.listener import EVENT_ID
+
+ self.register_as_listener(
+ proxy=ConfigurationManagerProxy,
+ listener={'name': 'Dummy CS', 'proxy': DummyProxy, 'event_id': EVENT_ID.SETUP}
+ )
+
+
def get_communication_protocol(self):
return 'tcp'
@@ -333,7 +475,15 @@ Inherited members
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):
+ return "DUMMY-HK"
+
+ def after_serve(self):
+ from egse.confman import ConfigurationManagerProxy
+
+ self.unregister_as_listener(proxy=ConfigurationManagerProxy, listener={'name': 'Dummy CS'})
Ancestors
@@ -341,6 +491,21 @@ Ancestors
Methods
+
+def after_serve (self)
+
+
+
+
+
+Expand source code
+
+def after_serve(self):
+ from egse.confman import ConfigurationManagerProxy
+
+ self.unregister_as_listener(proxy=ConfigurationManagerProxy, listener={'name': 'Dummy CS'})
+
+
def get_commanding_port (self)
@@ -393,15 +558,33 @@ Methods
return ctrl_settings.SERVICE_PORT
+
+def get_storage_mnemonic (self)
+
+
+
+
+
+Expand source code
+
+def get_storage_mnemonic(self):
+ return "DUMMY-HK"
+
+
Inherited members
ControlServer
:
@@ -409,20 +592,59 @@ Inherited members
class DummyController
+( control_server)
-
+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 DummyController:
+class DummyController(DummyInterface, EventInterface):
+ def __init__(self, control_server):
+ self._cs = control_server
+
def info(self):
return "method info() called on DummyController"
def response(self, *args, **kwargs):
- return f"response({args}, {kwargs})"
+ return f"response({args}, {kwargs})"
+
+ def handle_event(self, event: Event) -> str:
+
+ _exec_in_thread = False
+
+ def _handle_event(event):
+ LOGGER.info(f"An event is received, {event=}")
+ LOGGER.info(f"CM CS active? {is_configuration_manager_active()}")
+ time.sleep(5.0)
+ LOGGER.info(f"CM CS active? {is_configuration_manager_active()}")
+ LOGGER.info(f"An event is processed, {event=}")
+
+ if _exec_in_thread:
+ # We execute this function in a daemon thread so the acknowledgment is
+ # sent back immediately (the ACK means 'command received and will be
+ # executed).
+
+ retry_thread = threading.Thread(target=_handle_event, args=(event,))
+ retry_thread.daemon = True
+ retry_thread.start()
+ else:
+ # An alternative to the daemon thread is to create a scheduled task that will be executed
+ # after the event is acknowledged.
+
+ self._cs.schedule_task(partial(_handle_event, event))
+
+ return "ACK"
+Ancestors
+
Methods
@@ -452,6 +674,68 @@ Methods
+Inherited members
+
+
+
+class DummyInterface
+
+
+
+
+
+Expand source code
+
+class DummyInterface:
+ @dynamic_interface
+ def info(self):
+ ...
+ @dynamic_interface
+ def response(self, *args, **kwargs):
+ ...
+
+Subclasses
+
+Methods
+
+
+def info (self)
+
+
+
+
+
+Expand source code
+
+@dynamic_interface
+def info(self):
+ ...
+
+
+
+def response (self, *args, **kwargs)
+
+
+
+
+
+Expand source code
+
+@dynamic_interface
+def response(self, *args, **kwargs):
+ ...
+
+
+
class DummyProtocol
@@ -476,12 +760,14 @@ Methods
super().__init__()
self.control_server = control_server
- self.device_controller = DummyController()
+ self.device_controller = DummyController(control_server)
self.load_commands(commands, DummyCommand, DummyController)
self.build_device_method_lookup_table(self.device_controller)
+ self._count = 0
+
def get_bind_address(self):
return bind_address(self.control_server.get_communication_protocol(), self.control_server.get_commanding_port())
@@ -489,8 +775,22 @@ Methods
return super().get_status()
def get_housekeeping(self) -> dict:
+
+ # LOGGER.debug(f"Executing get_housekeeping function for {self.__class__.__name__}.")
+
+ self._count += 1
+
+ # use the sleep to test the responsiveness of the control server when even this get_housekeeping function takes
+ # a lot of time, i.e. up to several minutes in the case of data acquisition devices
+ # import time
+ # time.sleep(2.0)
+
+
return {
'timestamp': format_datetime(),
+ 'COUNT': self._count,
+ 'PI': 3.14159, # just to have a constant parameter
+ 'Random': random.randint(0, 100), # just to have a variable parameter
}
Ancestors
@@ -541,7 +841,7 @@ Args
Expand source code
-class DummyProxy(Proxy):
+class DummyProxy(Proxy, DummyInterface, EventInterface):
def __init__(self,
protocol=ctrl_settings.PROTOCOL, hostname=ctrl_settings.HOSTNAME, port=ctrl_settings.COMMANDING_PORT):
"""
@@ -550,13 +850,15 @@ Args
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))
+ super().__init__(connect_address(protocol, hostname, port), timeout=ctrl_settings.TIMEOUT)
Ancestors
Inherited members
@@ -579,6 +881,11 @@ Inherited members
send
+EventInterface
:
+
+
@@ -595,6 +902,11 @@
Index
egse
+
Functions
+
+
Classes
@@ -603,10 +915,12 @@ Dum
DummyControlServer
@@ -617,6 +931,13 @@ DummyInterface
+
+
+
DummyProtocol
diff --git a/docs/api/egse/dyndummy.html b/docs/api/egse/dyndummy.html
index 2eab277..f1d44d2 100644
--- a/docs/api/egse/dyndummy.html
+++ b/docs/api/egse/dyndummy.html
@@ -337,10 +337,15 @@ Inherited members
ControlServer
:
diff --git a/docs/api/egse/env.html b/docs/api/egse/env.html
index b909cc3..de702d8 100644
--- a/docs/api/egse/env.html
+++ b/docs/api/egse/env.html
@@ -74,12 +74,12 @@ Module egse.env
"""
import os
+ from egse.setup import load_setup
if site_id is None:
from egse.setup import Setup
- from egse.state import GlobalState
- setup: Setup = setup or GlobalState.setup
+ setup: Setup = setup or load_setup()
if setup is None:
raise ValueError("Could not determine Setup, which is None, even after loading from the configuration manager.")
@@ -152,6 +152,8 @@ Module egse.env
import sys
import rich
+ from egse.config import get_common_egse_root
+
parser = argparse.ArgumentParser()
parser.add_argument(
"--full",
@@ -209,6 +211,14 @@ Module egse.env
rich.print("Generated locations and filenames")
with all_logging_disabled():
+ try:
+ rich.print(f" {get_common_egse_root() = !s}", flush=True)
+ location = get_common_egse_root()
+ if not Path(location).exists():
+ rich.print("[red]ERROR: The generated Common-EGSE location doesn't exist![/]")
+ except ValueError as exc:
+ rich.print(f" get_common_egse_path() = [red]{exc}[/]")
+
try:
rich.print(f" {get_data_storage_location() = }", flush=True)
location = get_data_storage_location()
@@ -236,6 +246,7 @@ Module egse.env
if args.full:
rich.print()
rich.print(f" PYTHONPATH=[default]{os.environ.get('PYTHONPATH')}")
+ rich.print(f" PYTHONSTARTUP=[default]{os.environ.get('PYTHONSTARTUP')}")
rich.print()
python_path_msg = "\n ".join(sys.path)
rich.print(f" sys.path=[\n {python_path_msg}\n ]")
@@ -695,12 +706,12 @@ Raises
"""
import os
+ from egse.setup import load_setup
if site_id is None:
from egse.setup import Setup
- from egse.state import GlobalState
- setup: Setup = setup or GlobalState.setup
+ setup: Setup = setup or load_setup()
if setup is None:
raise ValueError("Could not determine Setup, which is None, even after loading from the configuration manager.")
diff --git a/docs/api/egse/exceptions.html b/docs/api/egse/exceptions.html
index 7a7643c..482ac1c 100644
--- a/docs/api/egse/exceptions.html
+++ b/docs/api/egse/exceptions.html
@@ -249,7 +249,8 @@ Subclasses
InternalStateError
InvalidInputError
InvalidOperationError
-RMAPError
+RMAPError
+CheckError
AlreadyRegisteredError
diff --git a/docs/api/egse/fdir/fdir_manager_controller.html b/docs/api/egse/fdir/fdir_manager_controller.html
index 1edb672..fc9f714 100644
--- a/docs/api/egse/fdir/fdir_manager_controller.html
+++ b/docs/api/egse/fdir/fdir_manager_controller.html
@@ -26,26 +26,17 @@ Module egse.fdir.fdir_manager_controller
Expand source code
-import os
-import re
-import logging
-import psutil
+import logging
+import os
import subprocess
-
-from threading import Thread
from datetime import datetime
-import yaml
-
-from PyQt5.QtWidgets import QMessageBox, QApplication, QDialog, QLabel, QVBoxLayout
-
+from egse.fdir import generate_popup
+from egse.fdir.fdir_remote import FdirRemoteProxy
+from egse.fdir.fdir_remote_interface import FdirRemoteInterface
from egse.settings import Settings
-from egse.setup import get_setup
+from egse.setup import load_setup
from egse.system import replace_environment_variable
-from egse.control import Success
-from egse.fdir.fdir_remote_interface import FdirRemoteInterface
-from egse.fdir.fdir_remote import FdirRemoteProxy
-from egse.fdir import generate_popup
logger = logging.getLogger(__name__)
@@ -63,27 +54,21 @@ Module egse.fdir.fdir_manager_controller
self._state = ""
self._priority = -1
- self._local_pids = []
- self._remote_pids = []
self._storage_path = replace_environment_variable(CTRL_SETTINGS.RECOVERY_SCRIPT_LOCATION)
self._logging_path = replace_environment_variable(CTRL_SETTINGS.LOGGING_LOCATION)
self._remote_proxy = FdirRemoteProxy()
# Check if the remote is online.
if not self._remote_proxy.is_cs_connected():
logger.warning("FDIR Manager could not connect to the FDIR remote CS")
- # raise RuntimeError("FDIR Manager could not connect the the FDIR Remote CS")
else:
logger.info(f"connected to remote CS @ {REMOTE_SETTINGS.HOSTNAME}")
# Try to load fdir table.
try:
- table_path = self._storage_path + "/fdir_table.yaml"
- with open(table_path) as f:
- self._fdir_table = yaml.load(f, Loader=yaml.Loader)
- except Exception as e:
- logger.error(f"Could not load FDIR table from {table_path}: {e}")
- raise e
-
+ setup = load_setup()
+ self._fdir_table = setup.gse.fdir_manager.configuration.table
+ except AttributeError as ex:
+ raise FdirException("Could not FDIR table from current setup") from ex
def is_connected(self):
return True
@@ -91,61 +76,6 @@ Module egse.fdir.fdir_manager_controller
def is_simulator(self):
return False
- def register_script(self, host: str, pid: int):
-
- if self._state != 0:
- logger.error(f"Attempt to register script while state is not idle ({self._state}).")
- raise FdirException(
- f"Cannot register script because FDIR state is not idle ({self._state}).")
-
- elif host in ['localhost', os.uname()[1]]:
-
- if pid in self._local_pids:
- logger.error(f"Attempt to register existing PID {pid} @ localhost.")
- raise FdirException("PID is already registered.")
- else:
- self._local_pids.append(pid)
- logger.info(f"Successful registration of PID {pid} @ localhost.")
-
- elif host == REMOTE_SETTINGS.HOSTNAME:
-
- if pid in self._remote_pids:
- logger.error(f"Attempt to register existing PID {pid} @ {host}.")
- raise FdirException("PID is already registered.")
- else:
- self._remote_pids.append(pid)
- logger.info(f"Successful registration of PID {pid} @ {host}.")
-
- else:
- logger.error(f"Attempt to register a script with unknown hostname ({host})")
- raise FdirException("unknown hostname")
-
-
- def deregister_script(self, host: str, pid: int):
-
- if host in ['localhost', os.uname()[1]]:
-
- if pid not in self._local_pids[host]:
- logger.error(f"Attempt to deregister unknown PID {pid} @ localhost.")
- raise FdirException("PID is unknown.")
- else:
- self._pids[host].remove(pid)
- logger.info(f"Successful deregistration of PID {pid} @ localhost.")
-
- if host == REMOTE_SETTINGS.HOSTNAME:
-
- if pid not in self._local_pids[host]:
- logger.error(f"Attempt to deregister unknown PID {pid} @ {host}.")
- raise FdirException("PID is unknown.")
- else:
- self._pids[host].remove(pid)
- logger.info(f"Successful deregistration of PID {pid} @ {host}.")
-
- else:
- logger.error(f"attempt to deregister a script with unknown hostname ({host})")
- raise FdirException("unknown hostname")
-
-
def signal_fdir(self, fdir_code: str, script_args: list):
logger.info(f"Received FDIR code {fdir_code}: {script_args}.")
@@ -167,30 +97,6 @@ Module egse.fdir.fdir_manager_controller
self._state = fdir_code
self._priority = priority
- # kill registered local scripts
- for pid in self._local_pids:
- try:
- process = psutil.Process(pid)
- except psutil.NoSuchProcess:
- logger.warning(
- f"Could not find a process with PID {pid} @ localhost.")
- else:
- process.kill()
- logger.info(f"Killed registered script with PID {pid} @ localhost.")
-
- # Kill registered remote scripts
- for pid in self._remote_pids:
- response = self._remote_proxy.kill_process(pid)
- if isinstance(response, Success):
- logger.info(f"Killed registered script with PID {pid} @ {REMOTE_SETTINGS.HOSTNAME}.")
- else:
- logger.warning(
- f"Could not kill process with PID {pid} @ {REMOTE_SETTINGS.HOSTNAME}.")
-
- # Clear all stored PIDs
- self._local_pids = []
- self._remote_pids = []
-
# open filewriter for fdir script output logging
timestring = datetime.now().strftime("%y-%m-%d_%H:%M:%S")
outfilename = f"{self._logging_path}/log/{timestring}_fdir_{fdir_code}"
@@ -235,13 +141,11 @@ Module egse.fdir.fdir_manager_controller
generate_popup(code=fdir_code, actions=action_string, success=script_success)
def clear_fdir(self):
-
self._priority = 0
logger.info("Cleared FDIR state.")
def get_state(self):
-
return self._priority
@@ -327,27 +231,21 @@ Ancestors
self._state = ""
self._priority = -1
- self._local_pids = []
- self._remote_pids = []
self._storage_path = replace_environment_variable(CTRL_SETTINGS.RECOVERY_SCRIPT_LOCATION)
self._logging_path = replace_environment_variable(CTRL_SETTINGS.LOGGING_LOCATION)
self._remote_proxy = FdirRemoteProxy()
# Check if the remote is online.
if not self._remote_proxy.is_cs_connected():
logger.warning("FDIR Manager could not connect to the FDIR remote CS")
- # raise RuntimeError("FDIR Manager could not connect the the FDIR Remote CS")
else:
logger.info(f"connected to remote CS @ {REMOTE_SETTINGS.HOSTNAME}")
# Try to load fdir table.
try:
- table_path = self._storage_path + "/fdir_table.yaml"
- with open(table_path) as f:
- self._fdir_table = yaml.load(f, Loader=yaml.Loader)
- except Exception as e:
- logger.error(f"Could not load FDIR table from {table_path}: {e}")
- raise e
-
+ setup = load_setup()
+ self._fdir_table = setup.gse.fdir_manager.configuration.table
+ except AttributeError as ex:
+ raise FdirException("Could not FDIR table from current setup") from ex
def is_connected(self):
return True
@@ -355,61 +253,6 @@ Ancestors
def is_simulator(self):
return False
- def register_script(self, host: str, pid: int):
-
- if self._state != 0:
- logger.error(f"Attempt to register script while state is not idle ({self._state}).")
- raise FdirException(
- f"Cannot register script because FDIR state is not idle ({self._state}).")
-
- elif host in ['localhost', os.uname()[1]]:
-
- if pid in self._local_pids:
- logger.error(f"Attempt to register existing PID {pid} @ localhost.")
- raise FdirException("PID is already registered.")
- else:
- self._local_pids.append(pid)
- logger.info(f"Successful registration of PID {pid} @ localhost.")
-
- elif host == REMOTE_SETTINGS.HOSTNAME:
-
- if pid in self._remote_pids:
- logger.error(f"Attempt to register existing PID {pid} @ {host}.")
- raise FdirException("PID is already registered.")
- else:
- self._remote_pids.append(pid)
- logger.info(f"Successful registration of PID {pid} @ {host}.")
-
- else:
- logger.error(f"Attempt to register a script with unknown hostname ({host})")
- raise FdirException("unknown hostname")
-
-
- def deregister_script(self, host: str, pid: int):
-
- if host in ['localhost', os.uname()[1]]:
-
- if pid not in self._local_pids[host]:
- logger.error(f"Attempt to deregister unknown PID {pid} @ localhost.")
- raise FdirException("PID is unknown.")
- else:
- self._pids[host].remove(pid)
- logger.info(f"Successful deregistration of PID {pid} @ localhost.")
-
- if host == REMOTE_SETTINGS.HOSTNAME:
-
- if pid not in self._local_pids[host]:
- logger.error(f"Attempt to deregister unknown PID {pid} @ {host}.")
- raise FdirException("PID is unknown.")
- else:
- self._pids[host].remove(pid)
- logger.info(f"Successful deregistration of PID {pid} @ {host}.")
-
- else:
- logger.error(f"attempt to deregister a script with unknown hostname ({host})")
- raise FdirException("unknown hostname")
-
-
def signal_fdir(self, fdir_code: str, script_args: list):
logger.info(f"Received FDIR code {fdir_code}: {script_args}.")
@@ -431,30 +274,6 @@ Ancestors
self._state = fdir_code
self._priority = priority
- # kill registered local scripts
- for pid in self._local_pids:
- try:
- process = psutil.Process(pid)
- except psutil.NoSuchProcess:
- logger.warning(
- f"Could not find a process with PID {pid} @ localhost.")
- else:
- process.kill()
- logger.info(f"Killed registered script with PID {pid} @ localhost.")
-
- # Kill registered remote scripts
- for pid in self._remote_pids:
- response = self._remote_proxy.kill_process(pid)
- if isinstance(response, Success):
- logger.info(f"Killed registered script with PID {pid} @ {REMOTE_SETTINGS.HOSTNAME}.")
- else:
- logger.warning(
- f"Could not kill process with PID {pid} @ {REMOTE_SETTINGS.HOSTNAME}.")
-
- # Clear all stored PIDs
- self._local_pids = []
- self._remote_pids = []
-
# open filewriter for fdir script output logging
timestring = datetime.now().strftime("%y-%m-%d_%H:%M:%S")
outfilename = f"{self._logging_path}/log/{timestring}_fdir_{fdir_code}"
@@ -499,13 +318,11 @@ Ancestors
generate_popup(code=fdir_code, actions=action_string, success=script_success)
def clear_fdir(self):
-
self._priority = 0
logger.info("Cleared FDIR state.")
def get_state(self):
-
return self._priority
Ancestors
@@ -524,45 +341,10 @@ Methods
Expand source code
def clear_fdir(self):
-
self._priority = 0
logger.info("Cleared FDIR state.")
-
-def deregister_script (self, host: str, pid: int)
-
-
-
-
-
-Expand source code
-
-def deregister_script(self, host: str, pid: int):
-
- if host in ['localhost', os.uname()[1]]:
-
- if pid not in self._local_pids[host]:
- logger.error(f"Attempt to deregister unknown PID {pid} @ localhost.")
- raise FdirException("PID is unknown.")
- else:
- self._pids[host].remove(pid)
- logger.info(f"Successful deregistration of PID {pid} @ localhost.")
-
- if host == REMOTE_SETTINGS.HOSTNAME:
-
- if pid not in self._local_pids[host]:
- logger.error(f"Attempt to deregister unknown PID {pid} @ {host}.")
- raise FdirException("PID is unknown.")
- else:
- self._pids[host].remove(pid)
- logger.info(f"Successful deregistration of PID {pid} @ {host}.")
-
- else:
- logger.error(f"attempt to deregister a script with unknown hostname ({host})")
- raise FdirException("unknown hostname")
-
-
def get_state (self)
@@ -573,7 +355,6 @@ Methods
Expand source code
def get_state(self):
-
return self._priority
@@ -603,45 +384,6 @@ Methods
return False
-
-def register_script (self, host: str, pid: int)
-
-
-
-
-
-Expand source code
-
-def register_script(self, host: str, pid: int):
-
- if self._state != 0:
- logger.error(f"Attempt to register script while state is not idle ({self._state}).")
- raise FdirException(
- f"Cannot register script because FDIR state is not idle ({self._state}).")
-
- elif host in ['localhost', os.uname()[1]]:
-
- if pid in self._local_pids:
- logger.error(f"Attempt to register existing PID {pid} @ localhost.")
- raise FdirException("PID is already registered.")
- else:
- self._local_pids.append(pid)
- logger.info(f"Successful registration of PID {pid} @ localhost.")
-
- elif host == REMOTE_SETTINGS.HOSTNAME:
-
- if pid in self._remote_pids:
- logger.error(f"Attempt to register existing PID {pid} @ {host}.")
- raise FdirException("PID is already registered.")
- else:
- self._remote_pids.append(pid)
- logger.info(f"Successful registration of PID {pid} @ {host}.")
-
- else:
- logger.error(f"Attempt to register a script with unknown hostname ({host})")
- raise FdirException("unknown hostname")
-
-
def signal_fdir (self, fdir_code: str, script_args: list)
@@ -672,30 +414,6 @@ Methods
self._state = fdir_code
self._priority = priority
- # kill registered local scripts
- for pid in self._local_pids:
- try:
- process = psutil.Process(pid)
- except psutil.NoSuchProcess:
- logger.warning(
- f"Could not find a process with PID {pid} @ localhost.")
- else:
- process.kill()
- logger.info(f"Killed registered script with PID {pid} @ localhost.")
-
- # Kill registered remote scripts
- for pid in self._remote_pids:
- response = self._remote_proxy.kill_process(pid)
- if isinstance(response, Success):
- logger.info(f"Killed registered script with PID {pid} @ {REMOTE_SETTINGS.HOSTNAME}.")
- else:
- logger.warning(
- f"Could not kill process with PID {pid} @ {REMOTE_SETTINGS.HOSTNAME}.")
-
- # Clear all stored PIDs
- self._local_pids = []
- self._remote_pids = []
-
# open filewriter for fdir script output logging
timestring = datetime.now().strftime("%y-%m-%d_%H:%M:%S")
outfilename = f"{self._logging_path}/log/{timestring}_fdir_{fdir_code}"
@@ -768,13 +486,11 @@ FdirManagerController
-
diff --git a/docs/api/egse/fdir/fdir_manager_cs.html b/docs/api/egse/fdir/fdir_manager_cs.html
index 56f32f4..e2e3dce 100644
--- a/docs/api/egse/fdir/fdir_manager_cs.html
+++ b/docs/api/egse/fdir/fdir_manager_cs.html
@@ -421,10 +421,15 @@ Inherited members
ControlServer
:
diff --git a/docs/api/egse/fdir/fdir_manager_interface.html b/docs/api/egse/fdir/fdir_manager_interface.html
index 7c5fe86..c66880c 100644
--- a/docs/api/egse/fdir/fdir_manager_interface.html
+++ b/docs/api/egse/fdir/fdir_manager_interface.html
@@ -27,19 +27,9 @@ Module egse.fdir.fdir_manager_interface
Expand source code
from egse.decorators import dynamic_interface
-from egse.control import Response, Success
class FdirManagerInterface:
""" Descriptions of the interface can be found in the corresponding yaml file. """
-
- @dynamic_interface
- def register_script(self, host: str, pid: int):
- raise NotImplementedError
-
- @dynamic_interface
- def deregister_script(self, host: str, pid: int):
- raise NotImplementedError
-
@dynamic_interface
def signal_fdir(self, fdir_code: int):
raise NotImplementedError
@@ -73,15 +63,6 @@
class FdirManagerInterface:
""" Descriptions of the interface can be found in the corresponding yaml file. """
-
- @dynamic_interface
- def register_script(self, host: str, pid: int):
- raise NotImplementedError
-
- @dynamic_interface
- def deregister_script(self, host: str, pid: int):
- raise NotImplementedError
-
@dynamic_interface
def signal_fdir(self, fdir_code: int):
raise NotImplementedError
@@ -114,20 +95,6 @@ Methods
raise NotImplementedError
-
-def deregister_script (self, host: str, pid: int)
-
-
-
-
-
-Expand source code
-
-@dynamic_interface
-def deregister_script(self, host: str, pid: int):
- raise NotImplementedError
-
-
def get_state (self)
@@ -142,20 +109,6 @@ Methods
raise NotImplementedError
-
-def register_script (self, host: str, pid: int)
-
-
-
-
-
-Expand source code
-
-@dynamic_interface
-def register_script(self, host: str, pid: int):
- raise NotImplementedError
-
-
def signal_fdir (self, fdir_code: int)
@@ -192,9 +145,7 @@ Index
FdirManagerInterface
diff --git a/docs/api/egse/fdir/fdir_remote_controller.html b/docs/api/egse/fdir/fdir_remote_controller.html
index a84977f..1184ed5 100644
--- a/docs/api/egse/fdir/fdir_remote_controller.html
+++ b/docs/api/egse/fdir/fdir_remote_controller.html
@@ -27,18 +27,11 @@ Module egse.fdir.fdir_remote_controller
Expand source code
import os
-import sys
import logging
-import psutil
import subprocess
-import threading
-import time
-from PyQt5.QtWidgets import QMessageBox, QApplication
-
-from egse.control import Response, Success, Failure
+from egse.control import Response
from egse.fdir.fdir_remote_interface import FdirRemoteInterface
-# from egse.fdir.fdir_manager import FdirManagerProxy
logger = logging.getLogger(__name__)
@@ -52,25 +45,12 @@ Module egse.fdir.fdir_remote_controller
def is_simulator(self):
return False
-
- def kill_process(self, pid: int) -> Response:
-
- try:
- process = psutil.Process(pid)
- except psutil.NoSuchProcess:
- logger.warning(f"Could not find a process with PID {pid}.")
- return Failure(f"Could not find a process with PID {pid}.")
- else:
- process.kill()
- logger.info(f"Killed registered script with PID {pid}.")
- return Success("")
def generate_popup(self, code: str, actions: str, success: bool) -> Response:
-
path = os.path.dirname(os.path.abspath(__file__))
subprocess.Popen(['python3', f'{path}/fdir_remote_popup.py', code, actions, str(success)])
-
+
if __name__ == "__main__":
@@ -105,21 +85,8 @@
def is_simulator(self):
return False
-
- def kill_process(self, pid: int) -> Response:
-
- try:
- process = psutil.Process(pid)
- except psutil.NoSuchProcess:
- logger.warning(f"Could not find a process with PID {pid}.")
- return Failure(f"Could not find a process with PID {pid}.")
- else:
- process.kill()
- logger.info(f"Killed registered script with PID {pid}.")
- return Success("")
def generate_popup(self, code: str, actions: str, success: bool) -> Response:
-
path = os.path.dirname(os.path.abspath(__file__))
subprocess.Popen(['python3', f'{path}/fdir_remote_popup.py', code, actions, str(success)])
@@ -139,7 +106,6 @@ Methods
Expand source code
def generate_popup(self, code: str, actions: str, success: bool) -> Response:
-
path = os.path.dirname(os.path.abspath(__file__))
subprocess.Popen(['python3', f'{path}/fdir_remote_popup.py', code, actions, str(success)])
@@ -170,28 +136,6 @@ Methods
return False
-
-def kill_process (self, pid: int) ‑> Response
-
-
-
-
-
-Expand source code
-
-def kill_process(self, pid: int) -> Response:
-
- try:
- process = psutil.Process(pid)
- except psutil.NoSuchProcess:
- logger.warning(f"Could not find a process with PID {pid}.")
- return Failure(f"Could not find a process with PID {pid}.")
- else:
- process.kill()
- logger.info(f"Killed registered script with PID {pid}.")
- return Success("")
-
-
@@ -216,7 +160,6 @@ generate_popup
is_connected
is_simulator
-
kill_process
diff --git a/docs/api/egse/fdir/fdir_remote_cs.html b/docs/api/egse/fdir/fdir_remote_cs.html
index 7c37942..c0b6e33 100644
--- a/docs/api/egse/fdir/fdir_remote_cs.html
+++ b/docs/api/egse/fdir/fdir_remote_cs.html
@@ -29,16 +29,13 @@
Module egse.fdir.fdir_remote_cs
import logging
import multiprocessing
import sys
-from pathlib import Path
-
-from PyQt5.QtWidgets import QApplication
import click
import zmq
+
from prometheus_client import start_http_server
from egse.settings import Settings
from egse.control import ControlServer
-from egse.system import replace_environment_variable
from egse.fdir.fdir_remote import FdirRemoteProtocol, FdirRemoteProxy
logger = logging.getLogger(__name__)
@@ -275,10 +272,15 @@ Inherited members
ControlServer
:
diff --git a/docs/api/egse/fdir/fdir_remote_interface.html b/docs/api/egse/fdir/fdir_remote_interface.html
index e4a6110..ecfafe3 100644
--- a/docs/api/egse/fdir/fdir_remote_interface.html
+++ b/docs/api/egse/fdir/fdir_remote_interface.html
@@ -27,16 +27,11 @@ Module egse.fdir.fdir_remote_interface
Expand source code
from egse.decorators import dynamic_interface
-from egse.control import Response, Success
+from egse.control import Response
class FdirRemoteInterface:
""" Descriptions of the interface can be found in the corresponding yaml file. """
-
- @dynamic_interface
- def kill_process(self, pid: int) -> Response:
- raise NotImplementedError
-
@dynamic_interface
def generate_popup(self, code: int, actions, success) -> Response:
raise NotImplementedError
@@ -62,11 +57,6 @@
class FdirRemoteInterface:
""" Descriptions of the interface can be found in the corresponding yaml file. """
-
- @dynamic_interface
- def kill_process(self, pid: int) -> Response:
- raise NotImplementedError
-
@dynamic_interface
def generate_popup(self, code: int, actions, success) -> Response:
raise NotImplementedError
@@ -93,20 +83,6 @@ Methods
raise NotImplementedError
-
-def kill_process (self, pid: int) ‑> Response
-
-
-
-
-
-Expand source code
-
-@dynamic_interface
-def kill_process(self, pid: int) -> Response:
- raise NotImplementedError
-
-
@@ -129,7 +105,6 @@
Index
FdirRemoteInterface
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
+
+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 @@
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 @@
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 @@
# 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 @@
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
-
-
@@ -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
-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
-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 @@
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 @@
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 @@
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 @@
# 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 @@
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 @@
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 @@
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 @@
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 @@
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_protocol
Expand 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_protocol
Module egse.filterwheel.eksma.fw8smc4_protocol
Classes
# 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 @@
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
ControlServer
:
diff --git a/docs/api/egse/filterwheel/eksma/fw8smc5_interface.html b/docs/api/egse/filterwheel/eksma/fw8smc5_interface.html
index 909d257..47e0dc0 100644
--- a/docs/api/egse/filterwheel/eksma/fw8smc5_interface.html
+++ b/docs/api/egse/filterwheel/eksma/fw8smc5_interface.html
@@ -181,8 +181,7 @@ Ancestors
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 @@
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 @@
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
@@ -1802,7 +1814,9 @@ Methods
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()
@@ -1926,7 +1940,11 @@ Methods
def set_lamp(self, state):
with BeagleboneProxy() as bb:
- bb.set_lamp(state)
+ bb.set_lamp(state)
+
+ def fix_controller_fault(self):
+ with BeagleboneProxy() as bb:
+ bb.fix_controller_fault()
Methods
@@ -1962,6 +1980,20 @@ Methods
return self.fw.disconnect()
+
+def fix_controller_fault (self)
+
+
+
+
+
+Expand source code
+
+def fix_controller_fault(self):
+ with BeagleboneProxy() as bb:
+ bb.fix_controller_fault()
+
+
def has_commands (self)
@@ -2143,7 +2175,7 @@ Methods
class OGSEUIView
-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
@@ -2208,6 +2240,7 @@ Methods
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)
@@ -2310,6 +2343,12 @@ Methods
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():
@@ -2425,6 +2464,22 @@ Class variables
Methods
+
+def clear_lamp_fault (self)
+
+
+
+
+
+Expand source code
+
+def clear_lamp_fault(self):
+ try:
+ self.actionObservers({'fix_controller_fault': []})
+ except Exception as e:
+ warning_popup("fix_controller_fault", e)
+
+
def decrease_intensity_wheel (self)
@@ -2555,6 +2610,7 @@ Methods
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)
@@ -2871,8 +2927,8 @@ Methods
( setup)
-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
@@ -3310,6 +3366,7 @@
decrease_position_wheel
disconnect
+fix_controller_fault
has_commands
home_filterwheel
increase_position_wheel
@@ -3331,6 +3388,7 @@ ROTATION_STEPS
TOTAL_POSITIONS
WHEEL_POSITIONS
+clear_lamp_fault
decrease_intensity_wheel
decrease_position_wheel
home_filterwheel
diff --git a/docs/api/egse/filterwheel/eksma/index.html b/docs/api/egse/filterwheel/eksma/index.html
index 29c082d..c4d2232 100644
--- a/docs/api/egse/filterwheel/eksma/index.html
+++ b/docs/api/egse/filterwheel/eksma/index.html
@@ -61,7 +61,32 @@ Module egse.filterwheel.eksma
```
-"""
+"""
+
+# Because of continued problems in generating the API documentation, we have decided to skip the
+# filterwheel docs because of the `ximc` library:
+#
+# ```
+# dlopen(/Users/rik/Documents/PyCharmProjects/plato-common-egse/src/egse/lib/ximc/libximc.framework/libximc, 0x0006):
+# tried: '/Users/rik/Documents/PyCharmProjects/plato-common-egse/src/egse/lib/ximc/libximc.framework/libximc' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')),
+# '/System/Volumes/Preboot/Cryptexes/OS/Users/rik/Documents/PyCharmProjects/plato-common-egse/src/egse/lib/ximc/libximc.framework/libximc' (no such file),
+# '/Users/rik/Documents/PyCharmProjects/plato-common-egse-2653/src/egse/lib/ximc/libximc.framework/libximc' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')),
+# '/Library/Frameworks/libximc.framework/libximc' (no such file),
+# '/System/Library/Frameworks/libximc.framework/libximc' (no such file, not in dyld cache)
+# Can\'t load libximc library. Please add all shared libraries to the appropriate places.
+# It is decribed in detail in developers' documentation. On Linux make sure you installed libximc-dev package.
+# make sure that the architecture of the system and the interpreter is the same
+# ```
+# See issue #2859
+#
+
+__pdoc__ = {
+ # 'fw8smc4': False,
+ 'fw8smc4_devif': False,
+ # 'fw8smc5': False,
+ 'fw8smc5_interface': False,
+ 'testpythonfw': False,
+}
@@ -72,18 +97,6 @@
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 @@
- egse.filterwheel.eksma.fw8smc5_controller
-
-
-
egse.filterwheel.eksma.fw8smc5_cs
-egse.filterwheel.eksma.fw8smc5_interface
-
-
-
egse.filterwheel.eksma.fw8smc5_simulator
@@ -112,10 +117,6 @@
- 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 @@
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 @@
self.hexapod_model = PunaUIModel(type_hexapod)
self.stages_model = HuberUIModel(type_stages)
+ self.setup = load_setup()
##############
# Connectivity
@@ -313,9 +316,9 @@
- "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 @@
# 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 @@
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, 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 @@
( 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])