1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
|
import yaml
import time
from qolab.file_utils import get_next_data_file
from qolab.tsdb import tsdb_append_metric_for_class_setter_or_getter
class BasicInstrument:
"""This is the most basic instrument class.
It is intended to be used as a parent class for real instruments.
Notable feature that when ``getConfig`` is called,
it gathers values of the properties listed in ``deviceProperties`` variable and
also if ``numberOfChannels`` is set, it gathers configs of channels listed in
``channelProperties``. The later case is intended for instruments with
global setting and channels, for example scopes. This is super handy
to include together with acquired and saved data to see under which condition
they are taken.
Some property might have ``tsdb_append`` decorator
which sends the property value to the time
series database (TSDB) for the log keeping. Use it only for properties
which are scalar, for example VoltsPerDiv, SampleRate, and similar.
Such logs could be later analyzed with a help of systems like Grafana
or anything which could read TSDB entries.
Parameters
----------
device_nickname : None (default) or str
used to distinguish similar (in hardware, e.g. scopes) instruments,
also used as a tag in Time Series DB (TSDB) logging.
If not set we fallback to 'Device type' in config.
tsdb_ingester : None or tsdb ingester
used to log properties (with setter/getter) to TSDB.
If 'None', the log entry is skipped
config : dictionary, default is empty dictionary
used to add or override default entries describing instrument
It is good idea to set the following keys in the dictionary:
'Device type', 'Device model', 'DeviceId', 'DeviceNickname',
'FnamePrefix', 'SavePath'.
Example
-------
>>> config['Device type'] = 'Basic Instrument'
>>> config['Device model'] = 'Model is unset'
>>> config['DeviceId'] = None
>>> config['DeviceNickname'] = device_nickname; # to separate similar instruments
>>> config['FnamePrefix'] = 'basicInstrument'
>>> config['SavePath'] = './data'
"""
def __init__(self, config={}, device_nickname=None, tsdb_ingester=None):
self.config = {}
self.config["Device type"] = "Basic Instrument"
self.config["Device model"] = "Model is unset"
self.config["DeviceId"] = None
self.config["DeviceNickname"] = device_nickname
# to separate similar instruments
self.config["FnamePrefix"] = "basicInstrument"
self.config["SavePath"] = "./data"
for k, v in config.items():
self.config[k] = v
self.tsdb_ingester = tsdb_ingester
# deviceProperties must have 'get' and preferably 'set' methods available,
# i.e. 'SampleRate' needs getSampleRate() and love to have setSampleRate(value)
# they will be used to obtain config and set device according to it
# self.deviceProperties = {'SampleRate', 'TimePerDiv', 'TrigDelay', };
self.deviceProperties = {"TimeStamp"}
def __repr__(self):
s = ""
s += f"{self.__class__.__name__}("
s += "config={"
cstr = []
for k, v in self.config.items():
if v is not None:
cstr.append(f"'{k}'" ": " f"'{v}'")
s += ", ".join(cstr)
s += "}"
if self.tsdb_ingester is not None:
s += f", tsdb_ingester={self.tsdb_ingester}"
s += ")"
return s
def tsdb_append(f):
"""Append value to TSDB. Intended as decorator for setters and getters."""
return tsdb_append_metric_for_class_setter_or_getter()(f)
def getChannelsConfig(self):
chconfig = {}
if not hasattr(self, "numberOfChannels"):
return chconfig
for chNum in range(1, self.numberOfChannels + 1):
chNconfig = {}
for p in self.channelProperties:
getter = f"getChan{p}"
if not hasattr(self, getter):
print(f"warning no getter for {p}, i.e. {getter} is missing")
continue
res = getattr(self, getter)(chNum)
chNconfig[p] = res
chconfig[chNum] = chNconfig
return chconfig
def getConfig(self):
config = self.config.copy()
dconfig = {}
for p in self.deviceProperties:
if hasattr(self, p) or hasattr(type(self), p):
dconfig[p] = getattr(self, p)
continue
getter = f"get{p}"
if not hasattr(self, getter):
print(f"warning no getter for {p}, i.e. {getter} is missing")
continue
res = getattr(self, getter)()
dconfig[p] = res
config["DeviceConfig"] = dconfig
if not hasattr(self, "numberOfChannels"):
return config
config["ChannelsConfig"] = self.getChannelsConfig()
return config
def setConfig(self, cfg):
new_config = cfg.copy()
device_config = new_config.pop("DeviceConfig")
self.config.update(new_config)
for p, v in device_config.items():
setter = f"set{p}"
if hasattr(self, setter):
# we have something like setProperty
getattr(self, setter)(v)
continue
if hasattr(self, p) or hasattr(type(self), p):
# we have attribute Property
setattr(self, p, v)
continue
print(f"warning: both {setter} and attribute {p} are missing, skipping {p}")
# self.deviceProperties.add(p)
# setattr(self, p, v)
def getTimeStamp(self):
return time.strftime("%Y/%m/%d %H:%M:%S")
def getHeader(self):
header = yaml.dump(self.getConfig(), default_flow_style=False, sort_keys=False)
header = header.split("\n")
return header
def getNextDataFile(self, extension="dat"):
fname = get_next_data_file(
self.config["FnamePrefix"], self.config["SavePath"], extension=extension
)
return fname
|