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/__init__.py +38 -38
- pytrms/_base/__init__.py +24 -24
- pytrms/_base/ioniclient.py +32 -32
- pytrms/_base/mqttclient.py +119 -119
- pytrms/_version.py +26 -26
- pytrms/clients/__init__.py +33 -33
- pytrms/clients/db_api.py +200 -183
- pytrms/clients/ioniclient.py +87 -87
- pytrms/clients/modbus.py +532 -528
- pytrms/clients/mqtt.py +800 -797
- pytrms/clients/ssevent.py +82 -82
- pytrms/compose/__init__.py +2 -2
- pytrms/compose/composition.py +302 -302
- pytrms/data/IoniTofPrefs.ini +112 -112
- pytrms/data/ParaIDs.csv +731 -731
- pytrms/helpers.py +126 -120
- pytrms/instrument.py +119 -119
- pytrms/measurement.py +173 -173
- pytrms/peaktable.py +499 -501
- pytrms/plotting/__init__.py +4 -4
- pytrms/plotting/plotting.py +27 -27
- pytrms/readers/__init__.py +4 -4
- pytrms/readers/ionitof_reader.py +472 -472
- {pytrms-0.9.2.dist-info → pytrms-0.9.3.dist-info}/LICENSE +339 -339
- {pytrms-0.9.2.dist-info → pytrms-0.9.3.dist-info}/METADATA +3 -2
- pytrms-0.9.3.dist-info/RECORD +27 -0
- {pytrms-0.9.2.dist-info → pytrms-0.9.3.dist-info}/WHEEL +1 -1
- pytrms-0.9.2.dist-info/RECORD +0 -27
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=
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
import
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
for
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
|