pytrms 0.9.2__py3-none-any.whl → 0.9.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pytrms/helpers.py CHANGED
@@ -1,120 +1,126 @@
1
- """@file helpers.py
2
-
3
- common helper functions.
4
- """
5
-
6
- def convert_labview_to_posix(lv_time_utc, utc_offset_sec):
7
- '''Create a `pandas.Timestamp` from LabView time.'''
8
- from pandas import Timestamp
9
-
10
- # change epoch from 01.01.1904 to 01.01.1970:
11
- posix_time = lv_time_utc - 2082844800
12
- # the tz must be specified in isoformat like '+02:30'..
13
- tz_sec = int(utc_offset_sec)
14
- tz_designator = '{0}{1:02d}:{2:02d}'.format(
15
- '+' if tz_sec >= 0 else '-', tz_sec // 3600, tz_sec % 3600 // 60)
16
-
17
- return Timestamp(posix_time, unit='s', tz=tz_designator)
18
-
19
-
20
- def parse_presets_file(presets_file):
21
- '''Load a `presets_file` as XML-tree and interpret the "OP_Mode" of this `Composition`.
22
-
23
- The tricky thing is, that any OP_Mode may or may not override previous settings!
24
- Therefore, it depends on the order of modes in this Composition to be able to assign
25
- each OP_Mode its actual dictionary of set_values.
26
-
27
- Note, that the preset file uses its own naming convention that cannot neccessarily be
28
- translated into standard parID-names. You may choose whatever you like to do with it.
29
- '''
30
- import xml.etree.ElementTree as ET
31
- from collections import namedtuple, defaultdict
32
-
33
- _key = namedtuple('preset_item', ['name', 'ads_path', 'dtype'])
34
- _parse_value = {
35
- "FLOAT": float,
36
- "BOOL": bool,
37
- "BYTE": int,
38
- "ENUM": int,
39
- }
40
- tree = ET.parse(presets_file)
41
- root = tree.getroot()
42
-
43
- preset_names = {}
44
- preset_items = defaultdict(dict)
45
- for index, preset in enumerate(root.iterfind('preset')):
46
- preset_names[index] = preset.find('name').text.strip()
47
-
48
- if preset.find('WritePrimIon').text.upper() == "TRUE":
49
- val = preset.find('IndexPrimIon').text
50
- preset_items[index][_key('PrimionIdx', '', 'INT')] = int(val)
51
-
52
- if preset.find('WriteTransmission').text.upper() == "TRUE":
53
- val = preset.find('IndexTransmission').text
54
- preset_items[index][_key('TransmissionIdx', '', 'INT')] = int(val)
55
-
56
- for item in preset.iterfind('item'):
57
- if item.find('Write').text.upper() == "TRUE":
58
- # device_index = item.find('DeviceIndex').text
59
- ads_path = item.find('AdsPath').text
60
- data_type = item.find('DataType').text
61
- # page_name = item.find('PageName').text
62
- name = item.find('Name').text
63
- value_text = item.find('Value').text
64
-
65
- key = _key(name, ads_path, data_type)
66
- val = _parse_value[data_type](value_text)
67
- preset_items[index][key] = val
68
-
69
- return {index: (preset_names[index], preset_items[index]) for index in preset_names.keys()}
70
-
71
-
72
- def setup_measurement_dir(config_dir=None, data_root_dir='D:/Data', suffix='',
73
- date_fmt = "%Y_%m_%d__%H_%M_%S"):
74
- """Create a new directory for saving the measurement and set it up.
75
-
76
- Optional: copy all files from the given config-directory.
77
-
78
- data_root_dir: the base folder for storing new measurement-directories
79
- suffix: will be appended to directory and data-file
80
- date_fmt: format for the source-folder and -file to be timestamped
81
- """
82
- import os
83
- import glob
84
- import shutil
85
- from collections import namedtuple
86
- from datetime import datetime
87
- from itertools import chain
88
-
89
- recipe = namedtuple('recipe', ['dirname', 'h5_file', 'pt_file', 'alarms_file'])
90
- _pt_formats = ['*.ionipt']
91
- _al_formats = ['*.alm']
92
- # make directory with current timestamp:
93
- now = datetime.now()
94
- new_h5_file = os.path.abspath(os.path.join(
95
- data_root_dir,
96
- now.strftime(date_fmt) + suffix,
97
- now.strftime(date_fmt) + suffix + '.h5',
98
- ))
99
- new_recipe_dir = os.path.dirname(new_h5_file)
100
- os.makedirs(new_recipe_dir, exist_ok=False) # may throw!
101
- if not config_dir:
102
- # we're done here..
103
- return recipe(new_recipe_dir, new_h5_file, '', '')
104
-
105
- # find the *first* matching file or an empty string if no match...
106
- new_pt_file = next(chain.from_iterable(glob.iglob(config_dir + "/" + g) for g in _pt_formats), '')
107
- new_al_file = next(chain.from_iterable(glob.iglob(config_dir + "/" + g) for g in _al_formats), '')
108
- # ...and copy all files from the master-recipe-dir:
109
- files2copy = glob.glob(config_dir + "/*")
110
- for file in files2copy:
111
- new_file = shutil.copy(file, new_recipe_dir)
112
- try: # remove write permission (a.k.a. make files read-only)
113
- mode = os.stat(file).st_mode
114
- os.chmod(new_file, mode & ~stat.S_IWRITE)
115
- except Exception as exc:
116
- # well, we can't set write permission
117
- pass
118
-
119
- return recipe(new_recipe_dir, new_h5_file, new_pt_file, new_al_file)
120
-
1
+ """@file helpers.py
2
+
3
+ common helper functions.
4
+ """
5
+
6
+ def convert_labview_to_posix(lv_time_utc, utc_offset_sec):
7
+ '''Create a `pandas.Timestamp` from LabView time.'''
8
+ from pandas import Timestamp
9
+
10
+ # change epoch from 01.01.1904 to 01.01.1970:
11
+ posix_time = lv_time_utc - 2082844800
12
+ # the tz must be specified in isoformat like '+02:30'..
13
+ tz_sec = int(utc_offset_sec)
14
+ tz_designator = '{0}{1:02d}:{2:02d}'.format(
15
+ '+' if tz_sec >= 0 else '-', tz_sec // 3600, tz_sec % 3600 // 60)
16
+
17
+ return Timestamp(posix_time, unit='s', tz=tz_designator)
18
+
19
+
20
+ def parse_presets_file(presets_file):
21
+ '''Load a `presets_file` as XML-tree and interpret the "OP_Mode" of this `Composition`.
22
+
23
+ The tricky thing is, that any OP_Mode may or may not override previous settings!
24
+ Therefore, it depends on the order of modes in this Composition to be able to assign
25
+ each OP_Mode its actual dictionary of set_values.
26
+
27
+ Note, that the preset file uses its own naming convention that cannot neccessarily be
28
+ translated into standard parID-names. You may choose whatever you like to do with it.
29
+ '''
30
+ import xml.etree.ElementTree as ET
31
+ from collections import namedtuple, defaultdict
32
+
33
+ _key = namedtuple('preset_item', ['name', 'ads_path', 'dtype'])
34
+ _parse_value = {
35
+ "FLOAT": float,
36
+ "BOOL": bool,
37
+ "BYTE": int,
38
+ "ENUM": int,
39
+ }
40
+ tree = ET.parse(presets_file)
41
+ root = tree.getroot()
42
+
43
+ preset_names = {}
44
+ preset_items = defaultdict(dict)
45
+ for index, preset in enumerate(root.iterfind('preset')):
46
+ preset_names[index] = preset.find('name').text.strip()
47
+
48
+ if preset.find('WritePrimIon').text.upper() == "TRUE":
49
+ val = preset.find('IndexPrimIon').text
50
+ preset_items[index][_key('PrimionIdx', '', 'INT')] = int(val)
51
+
52
+ if preset.find('WriteTransmission').text.upper() == "TRUE":
53
+ val = preset.find('IndexTransmission').text
54
+ preset_items[index][_key('TransmissionIdx', '', 'INT')] = int(val)
55
+
56
+ for item in preset.iterfind('item'):
57
+ if item.find('Write').text.upper() == "TRUE":
58
+ # device_index = item.find('DeviceIndex').text
59
+ ads_path = item.find('AdsPath').text
60
+ data_type = item.find('DataType').text
61
+ # page_name = item.find('PageName').text
62
+ name = item.find('Name').text
63
+ value_text = item.find('Value').text
64
+
65
+ key = _key(name, ads_path, data_type)
66
+ val = _parse_value[data_type](value_text)
67
+ preset_items[index][key] = val
68
+
69
+ return {index: (preset_names[index], preset_items[index]) for index in preset_names.keys()}
70
+
71
+
72
+ def setup_measurement_dir(config_dir=None, data_root_dir="D:/Data", suffix="",
73
+ date_fmt = "%Y_%m_%d__%H_%M_%S", exclude=r".*\.REPLAY"):
74
+ """Create a new directory for saving the measurement and set it up.
75
+
76
+ Optional: copy all files from the given config-directory.
77
+
78
+ config_dir - where to copy from
79
+ data_root_dir - the base folder for storing new measurement-directories
80
+ suffix - will be appended to directory and data-file
81
+ date_fmt - format for the source-folder and -file to be timestamped
82
+ exclude - a regular expression to exclude files from copying
83
+ """
84
+ import os
85
+ import re
86
+ import glob
87
+ import shutil
88
+ from collections import namedtuple
89
+ from datetime import datetime
90
+ from itertools import chain
91
+
92
+ recipe = namedtuple('recipe', ['dirname', 'h5_file', 'pt_file', 'alarms_file'])
93
+ _pt_formats = ['*.ionipt']
94
+ _al_formats = ['*.alm']
95
+ # make directory with current timestamp:
96
+ now = datetime.now()
97
+ new_h5_file = os.path.abspath(os.path.join(
98
+ data_root_dir,
99
+ now.strftime(date_fmt) + suffix,
100
+ now.strftime(date_fmt) + suffix + '.h5',
101
+ ))
102
+ new_recipe_dir = os.path.dirname(new_h5_file)
103
+ os.makedirs(new_recipe_dir, exist_ok=False) # may throw!
104
+ if not config_dir:
105
+ # we're done here..
106
+ return recipe(new_recipe_dir, new_h5_file, '', '')
107
+
108
+ # find the *first* matching file or an empty string if no match...
109
+ new_pt_file = next(chain.from_iterable(glob.iglob(config_dir + "/" + g) for g in _pt_formats), '')
110
+ new_al_file = next(chain.from_iterable(glob.iglob(config_dir + "/" + g) for g in _al_formats), '')
111
+ # ...and copy all files from the master-recipe-dir:
112
+ files2copy = glob.glob(config_dir + "/*")
113
+ for file in files2copy:
114
+ if exclude and re.match(exclude, file):
115
+ continue
116
+
117
+ new_file = shutil.copy(file, new_recipe_dir)
118
+ try: # remove write permission (a.k.a. make files read-only)
119
+ mode = os.stat(file).st_mode
120
+ os.chmod(new_file, mode & ~stat.S_IWRITE)
121
+ except Exception as exc:
122
+ # well, we can't set write permission
123
+ pass
124
+
125
+ return recipe(new_recipe_dir, new_h5_file, new_pt_file, new_al_file)
126
+
pytrms/instrument.py CHANGED
@@ -1,119 +1,119 @@
1
- import os.path
2
- import time
3
- from abc import abstractmethod, ABC
4
-
5
- from .measurement import *
6
-
7
-
8
- class Instrument(ABC):
9
- '''
10
- Class for controlling the PTR instrument remotely.
11
-
12
- This class reflects the states of the actual instrument, which can be currently idle
13
- or running a measurement. An idle instrument can start a measurement. A running
14
- instrument can be stopped. But trying to start a measurement twice will raise an
15
- exception (RuntimeError).
16
-
17
- Note, that for every client PTR instrument there is only one instance of this class.
18
- This is to prevent different instances to be in other states than the instrument.
19
- '''
20
- __instance = None
21
-
22
- def _new_state(self, newstate):
23
- # Note: we get ourselves a nifty little state-machine :)
24
- self.__class__ = newstate
25
-
26
- def __new__(cls, backend):
27
- # make this class a singleton..
28
- if cls._Instrument__instance is not None:
29
- # quick reminder: If __new__() does not return an instance of cls, then the
30
- # new instance’s __init__() method will *not* be invoked:
31
- return cls._Instrument__instance
32
-
33
- # ..that is synchronized with the PTR-instrument state:
34
- if backend.is_running:
35
- cls = RunningInstrument
36
- else:
37
- cls = IdleInstrument
38
-
39
- inst = object.__new__(cls)
40
- Instrument._Instrument__instance = inst
41
-
42
- return inst
43
-
44
- def __init__(self, backend):
45
- # dispatch all blocking calls to the client
46
- self.backend = backend
47
-
48
- @property
49
- def is_local(self):
50
- """Returns True if files are written to the local machine."""
51
- host = str(self.backend.host)
52
- return 'localhost' in host or '127.0.0.1' in host
53
-
54
- def get(self, varname):
55
- """Get the current value of a setting."""
56
- # TODO :: this is not an interface implementation
57
- raw = self.backend.get(varname)
58
- if not isinstance(self.backend, MqttClient):
59
- import json
60
- jobj = json.loads(raw)
61
-
62
- return jobj[0]['Act']['Real']
63
-
64
- ## how it should be:
65
- return raw
66
-
67
- def set(self, varname, value, unit='-'):
68
- """Set a variable to a new value."""
69
- return self.backend.set(varname, value, unit='-')
70
-
71
- _current_sourcefile = ''
72
-
73
- def start_measurement(self, filename=''):
74
- # this method must be implemented by each state
75
- raise RuntimeError("can't start %s" % self.__class__)
76
-
77
- def stop_measurement(self):
78
- # this method must be implemented by each state
79
- raise RuntimeError("can't stop %s" % self.__class__)
80
-
81
-
82
- class IdleInstrument(Instrument):
83
-
84
- def start_measurement(self, filename=''):
85
- dirname = os.path.dirname(filename)
86
- if dirname and self.is_local:
87
- # Note: if we send a filepath to the server that does not exist there, the
88
- # server will open a dialog and "hang" (which I'd very much like to avoid).
89
- # the safest way is to not send a path at all and start a 'Quick' measurement.
90
- # but if the server is the local machine, we do our best to verify the path:
91
- os.makedirs(dirname, exist_ok=True)
92
-
93
- if filename:
94
- basename = os.path.basename(filename)
95
- # this may very well be a directory to record a filename into:
96
- if not basename:
97
- basename = '%Y-%m-%d_%H-%M-%S.h5'
98
- filename = os.path.join(dirname, basename)
99
- # finally, pass everything through strftime...
100
- filename = time.strftime(filename)
101
- if os.path.exists(filename):
102
- raise RuntimeError(f'filename exists and cannot be overwritten')
103
-
104
- self.backend.start_measurement(filename)
105
- self._current_sourcefile = filename
106
- self._new_state(RunningInstrument)
107
-
108
- return RunningMeasurement(self)
109
-
110
-
111
- class RunningInstrument(Instrument):
112
-
113
- def stop_measurement(self):
114
- self.backend.stop_measurement()
115
- self._new_state(IdleInstrument)
116
-
117
- # TODO :: this catches only one sourcefile.. it'll do for simple cases:
118
- return FinishedMeasurement(_current_sourcefile)
119
-
1
+ import os.path
2
+ import time
3
+ from abc import abstractmethod, ABC
4
+
5
+ from .measurement import *
6
+
7
+
8
+ class Instrument(ABC):
9
+ '''
10
+ Class for controlling the PTR instrument remotely.
11
+
12
+ This class reflects the states of the actual instrument, which can be currently idle
13
+ or running a measurement. An idle instrument can start a measurement. A running
14
+ instrument can be stopped. But trying to start a measurement twice will raise an
15
+ exception (RuntimeError).
16
+
17
+ Note, that for every client PTR instrument there is only one instance of this class.
18
+ This is to prevent different instances to be in other states than the instrument.
19
+ '''
20
+ __instance = None
21
+
22
+ def _new_state(self, newstate):
23
+ # Note: we get ourselves a nifty little state-machine :)
24
+ self.__class__ = newstate
25
+
26
+ def __new__(cls, backend):
27
+ # make this class a singleton..
28
+ if cls._Instrument__instance is not None:
29
+ # quick reminder: If __new__() does not return an instance of cls, then the
30
+ # new instance’s __init__() method will *not* be invoked:
31
+ return cls._Instrument__instance
32
+
33
+ # ..that is synchronized with the PTR-instrument state:
34
+ if backend.is_running:
35
+ cls = RunningInstrument
36
+ else:
37
+ cls = IdleInstrument
38
+
39
+ inst = object.__new__(cls)
40
+ Instrument._Instrument__instance = inst
41
+
42
+ return inst
43
+
44
+ def __init__(self, backend):
45
+ # dispatch all blocking calls to the client
46
+ self.backend = backend
47
+
48
+ @property
49
+ def is_local(self):
50
+ """Returns True if files are written to the local machine."""
51
+ host = str(self.backend.host)
52
+ return 'localhost' in host or '127.0.0.1' in host
53
+
54
+ def get(self, varname):
55
+ """Get the current value of a setting."""
56
+ # TODO :: this is not an interface implementation
57
+ raw = self.backend.get(varname)
58
+ if not isinstance(self.backend, MqttClient):
59
+ import json
60
+ jobj = json.loads(raw)
61
+
62
+ return jobj[0]['Act']['Real']
63
+
64
+ ## how it should be:
65
+ return raw
66
+
67
+ def set(self, varname, value, unit='-'):
68
+ """Set a variable to a new value."""
69
+ return self.backend.set(varname, value, unit='-')
70
+
71
+ _current_sourcefile = ''
72
+
73
+ def start_measurement(self, filename=''):
74
+ # this method must be implemented by each state
75
+ raise RuntimeError("can't start %s" % self.__class__)
76
+
77
+ def stop_measurement(self):
78
+ # this method must be implemented by each state
79
+ raise RuntimeError("can't stop %s" % self.__class__)
80
+
81
+
82
+ class IdleInstrument(Instrument):
83
+
84
+ def start_measurement(self, filename=''):
85
+ dirname = os.path.dirname(filename)
86
+ if dirname and self.is_local:
87
+ # Note: if we send a filepath to the server that does not exist there, the
88
+ # server will open a dialog and "hang" (which I'd very much like to avoid).
89
+ # the safest way is to not send a path at all and start a 'Quick' measurement.
90
+ # but if the server is the local machine, we do our best to verify the path:
91
+ os.makedirs(dirname, exist_ok=True)
92
+
93
+ if filename:
94
+ basename = os.path.basename(filename)
95
+ # this may very well be a directory to record a filename into:
96
+ if not basename:
97
+ basename = '%Y-%m-%d_%H-%M-%S.h5'
98
+ filename = os.path.join(dirname, basename)
99
+ # finally, pass everything through strftime...
100
+ filename = time.strftime(filename)
101
+ if os.path.exists(filename):
102
+ raise RuntimeError(f'filename exists and cannot be overwritten')
103
+
104
+ self.backend.start_measurement(filename)
105
+ self._current_sourcefile = filename
106
+ self._new_state(RunningInstrument)
107
+
108
+ return RunningMeasurement(self)
109
+
110
+
111
+ class RunningInstrument(Instrument):
112
+
113
+ def stop_measurement(self):
114
+ self.backend.stop_measurement()
115
+ self._new_state(IdleInstrument)
116
+
117
+ # TODO :: this catches only one sourcefile.. it'll do for simple cases:
118
+ return FinishedMeasurement(_current_sourcefile)
119
+