egauge-python 0.9.8__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.
- egauge/ctid/__init__.py +7 -0
- egauge/ctid/bit_stuffer.py +65 -0
- egauge/ctid/ctid.py +967 -0
- egauge/ctid/encoder.py +436 -0
- egauge/ctid/intel_hex_encoder.py +98 -0
- egauge/ctid/waveform.py +299 -0
- egauge/examples/data/test-ctid-decoder.raw +0 -0
- egauge/examples/test_capture.py +77 -0
- egauge/examples/test_common.py +26 -0
- egauge/examples/test_ctid.py +89 -0
- egauge/examples/test_ctid_decoder.py +93 -0
- egauge/examples/test_local.py +201 -0
- egauge/examples/test_register.py +104 -0
- egauge/loggers.py +72 -0
- egauge/pyside/__init__.py +0 -0
- egauge/pyside/ansi2html.py +112 -0
- egauge/pyside/terminal.py +295 -0
- egauge/webapi/__init__.py +34 -0
- egauge/webapi/auth.py +364 -0
- egauge/webapi/cloud/__init__.py +30 -0
- egauge/webapi/cloud/credentials.py +86 -0
- egauge/webapi/cloud/credentials_dialog.py +58 -0
- egauge/webapi/cloud/gui/credentials_dialog.py +100 -0
- egauge/webapi/cloud/serial_number.py +276 -0
- egauge/webapi/device/__init__.py +38 -0
- egauge/webapi/device/capture.py +453 -0
- egauge/webapi/device/ctid_info.py +553 -0
- egauge/webapi/device/device.py +349 -0
- egauge/webapi/device/local.py +268 -0
- egauge/webapi/device/physical_quantity.py +439 -0
- egauge/webapi/device/physical_units.py +473 -0
- egauge/webapi/device/register.py +338 -0
- egauge/webapi/device/register_row.py +145 -0
- egauge/webapi/device/register_type.py +851 -0
- egauge/webapi/device/slop.py +334 -0
- egauge/webapi/device/virtual_register.py +353 -0
- egauge/webapi/error.py +34 -0
- egauge/webapi/json_api.py +332 -0
- egauge_python-0.9.8.dist-info/METADATA +148 -0
- egauge_python-0.9.8.dist-info/RECORD +44 -0
- egauge_python-0.9.8.dist-info/WHEEL +5 -0
- egauge_python-0.9.8.dist-info/entry_points.txt +2 -0
- egauge_python-0.9.8.dist-info/licenses/LICENSE +22 -0
- egauge_python-0.9.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2022-2024 eGauge Systems LLC
|
|
3
|
+
# 1644 Conestoga St, Suite 2
|
|
4
|
+
# Boulder, CO 80301
|
|
5
|
+
# voice: 720-545-9767
|
|
6
|
+
# email: davidm@egauge.net
|
|
7
|
+
#
|
|
8
|
+
# All rights reserved.
|
|
9
|
+
#
|
|
10
|
+
# MIT License
|
|
11
|
+
#
|
|
12
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
# in the Software without restriction, including without limitation the rights
|
|
15
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
# furnished to do so, subject to the following conditions:
|
|
18
|
+
#
|
|
19
|
+
# The above copyright notice and this permission notice shall be included in
|
|
20
|
+
# all copies or substantial portions of the Software.
|
|
21
|
+
#
|
|
22
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
28
|
+
# THE SOFTWARE.
|
|
29
|
+
#
|
|
30
|
+
"""This module provides access to the eGauge WebAPI's /api/capture
|
|
31
|
+
service."""
|
|
32
|
+
|
|
33
|
+
import time
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from decimal import Decimal
|
|
36
|
+
from enum import Enum
|
|
37
|
+
|
|
38
|
+
from ..error import Error
|
|
39
|
+
from .device import Device
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CaptureError(Error):
|
|
43
|
+
"""Base exception for all errors raised by this module."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ChannelSamples:
|
|
48
|
+
"""Each sample is stored as a pair (t,y) where t is the time the
|
|
49
|
+
sample was captured and y is value of the channel at that time.
|
|
50
|
+
|
|
51
|
+
The time t is in seconds relative to the trigger point. If the
|
|
52
|
+
sample occurred before the trigger point, t will be negative. If
|
|
53
|
+
there is no trigger point (i.e., the capture was initated
|
|
54
|
+
immediately or automatically), the time is relative to the first
|
|
55
|
+
sample.
|
|
56
|
+
|
|
57
|
+
The value y is in the physical unit of the channel it was captured
|
|
58
|
+
on. The physical unit of a channel can be obtained with method
|
|
59
|
+
Capture.channel_unit(). For ease of further processing, the
|
|
60
|
+
samples are stored as two lists of equal length: ts for the time
|
|
61
|
+
stamps and ys for the y values.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
ts: list[float] = field(default_factory=list)
|
|
66
|
+
ys: list[float] = field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class TriggerPoint:
|
|
71
|
+
"""A trigger point identifies the sample that first satisfied the
|
|
72
|
+
trigger condition. `channel` stores the name of the channel that
|
|
73
|
+
was the trigger channel and `index` stores the index of the sample
|
|
74
|
+
of that channel that stores the trigger sample.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
channel: str
|
|
79
|
+
index: int
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class CaptureResult:
|
|
84
|
+
"""A CaptureResult stores all data for a single capture.
|
|
85
|
+
|
|
86
|
+
`unix_ts` is the Unix timestamp of the sample that triggered the
|
|
87
|
+
capture, or the timestamp of the first captured sample, if the
|
|
88
|
+
capture was triggered immediately or automatically.
|
|
89
|
+
|
|
90
|
+
`samples` stores the samples for each channel that was captured,
|
|
91
|
+
indexed by channel name. Note that, even though each channel has
|
|
92
|
+
the same number of samples, the timestamps in those samples are
|
|
93
|
+
generally distinct. Use interpolation methods such as
|
|
94
|
+
scipy.interpolate.interp1d() to interpolate channel data to a
|
|
95
|
+
common point in time.
|
|
96
|
+
|
|
97
|
+
The relative time stamps stored in the samples can be converted to
|
|
98
|
+
absolute time stamps by adding them to `unix_ts`. To avoid loss
|
|
99
|
+
of precision, you may want to consider using Decimal() numbers for
|
|
100
|
+
this purpose: UNIX timestamps may take up 32 bits or more and
|
|
101
|
+
microsecond granularity requires another 20 bits, quickly
|
|
102
|
+
approaching the precision limits of a double-precision IEEE754
|
|
103
|
+
floating point number.
|
|
104
|
+
|
|
105
|
+
`trigger_point` identifies the first sample that satisfied the
|
|
106
|
+
trigger condition. If there was no such sample, it is None. Note
|
|
107
|
+
that it is possible to trigger on a channel that is not captured,
|
|
108
|
+
in which case the trigger sample is not actually stored in the
|
|
109
|
+
CaptureResult.
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
unix_ts: Decimal | None = None
|
|
114
|
+
samples: dict[str, ChannelSamples] = field(default_factory=dict)
|
|
115
|
+
trigger_point: TriggerPoint | None = None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TriggerMode(Enum):
|
|
119
|
+
"""The trigger mode determines how the trigger condition is
|
|
120
|
+
evaluated:
|
|
121
|
+
|
|
122
|
+
`ANY` triggers immediately.
|
|
123
|
+
|
|
124
|
+
`RISING` triggers when the trigger channel has a rising edge that
|
|
125
|
+
crosses the trigger level.
|
|
126
|
+
|
|
127
|
+
`FALLING` triggers when the trigger channel has a falling edge that
|
|
128
|
+
crosses the trigger level.
|
|
129
|
+
|
|
130
|
+
`ABOVE` triggers if the trigger channel has a value that is greater
|
|
131
|
+
than the trigger level.
|
|
132
|
+
|
|
133
|
+
`BELOW` triggers if the trigger channel has a value that is less the
|
|
134
|
+
trigger level.
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
ANY = "any"
|
|
139
|
+
RISING = "rise"
|
|
140
|
+
FALLING = "fall"
|
|
141
|
+
ABOVE = "gt"
|
|
142
|
+
BELOW = "lt"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Capture:
|
|
146
|
+
"""Objects of this class are used to setup the capture of waveform
|
|
147
|
+
data for one or more channels, capture data synchronously
|
|
148
|
+
or asynchronously, and to provide access to captured data in a
|
|
149
|
+
convenient fashion.
|
|
150
|
+
|
|
151
|
+
Properties:
|
|
152
|
+
channels: set(str)
|
|
153
|
+
The name of the channel that should be the trigger source
|
|
154
|
+
or None if the capture should be triggered immediately.
|
|
155
|
+
|
|
156
|
+
trigger_channel: str
|
|
157
|
+
The channel used to trigger the capture or None if
|
|
158
|
+
the capture should be triggered immediately.
|
|
159
|
+
|
|
160
|
+
trigger_mode: TriggerMode
|
|
161
|
+
The trigger mode or None if no the capture should be
|
|
162
|
+
triggered immediately.
|
|
163
|
+
|
|
164
|
+
trigger_level: float
|
|
165
|
+
The trigger level or None if no trigger level should be
|
|
166
|
+
set. The level is in the physical unit of the trigger
|
|
167
|
+
channel, as provided by Capture.channel_unit().
|
|
168
|
+
|
|
169
|
+
trigger_timeout: float
|
|
170
|
+
This is a timeout in seconds. An automatic trigger is
|
|
171
|
+
generated if the trigger condition does not occur within
|
|
172
|
+
this timeout.
|
|
173
|
+
|
|
174
|
+
trigger_pretrigger: float
|
|
175
|
+
This defines how much data is captured stored ahead of the
|
|
176
|
+
trigger point. It is a duration in seconds. A value of 0
|
|
177
|
+
causes the first sample to be the trigger point. A value
|
|
178
|
+
of 2 would mean that 2 seconds of data are captured before
|
|
179
|
+
the trigger point.
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def __init__(self, dev: Device):
|
|
184
|
+
"""Create a capture object for a meter.
|
|
185
|
+
|
|
186
|
+
Required arguments:
|
|
187
|
+
|
|
188
|
+
dev -- The device object for the meter.
|
|
189
|
+
|
|
190
|
+
"""
|
|
191
|
+
self.channels: set[str] = set()
|
|
192
|
+
self.trigger_channel: str | None = None
|
|
193
|
+
self.trigger_level: float | None = None
|
|
194
|
+
self.trigger_mode: TriggerMode | None = None
|
|
195
|
+
self.trigger_timeout: float | None = None
|
|
196
|
+
self.pretrigger: float | None = None
|
|
197
|
+
|
|
198
|
+
# private state:
|
|
199
|
+
self._dev = dev
|
|
200
|
+
self._ch_map = None
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def available_channels(self) -> list[str]:
|
|
204
|
+
"""Return the list of available channel names."""
|
|
205
|
+
return list(self._dev.channel_info().keys())
|
|
206
|
+
|
|
207
|
+
def channel_unit(self, channel_name: str) -> str:
|
|
208
|
+
"""Return the physical unit of a channel.
|
|
209
|
+
|
|
210
|
+
Required arguments:
|
|
211
|
+
|
|
212
|
+
channel_name -- The name of the channel.
|
|
213
|
+
|
|
214
|
+
"""
|
|
215
|
+
cinfo = self._dev.channel_info().get(channel_name)
|
|
216
|
+
if cinfo is None:
|
|
217
|
+
raise CaptureError("Unknown channel.", channel_name)
|
|
218
|
+
return cinfo.unit
|
|
219
|
+
|
|
220
|
+
def start(self, duration: float | None = None, **kwargs) -> int:
|
|
221
|
+
"""Initiate capturing samples.
|
|
222
|
+
|
|
223
|
+
This method returns an integer "cookie" which uniquely
|
|
224
|
+
identifies the pending capture. In the case of an error,
|
|
225
|
+
CaptureError is raised.
|
|
226
|
+
|
|
227
|
+
Keyword arguments:
|
|
228
|
+
|
|
229
|
+
duration -- The maximum duration in seconds for which samples
|
|
230
|
+
should be captured. If left unspecified, samples are
|
|
231
|
+
captured until the meter's sampling buffer is full.
|
|
232
|
+
|
|
233
|
+
Additional keyword arguments are passed on to requests.get().
|
|
234
|
+
|
|
235
|
+
"""
|
|
236
|
+
params = "n"
|
|
237
|
+
|
|
238
|
+
if duration is not None:
|
|
239
|
+
params += f"&d={duration}"
|
|
240
|
+
|
|
241
|
+
for name in self.channels:
|
|
242
|
+
ch = self._ch_number(name)
|
|
243
|
+
params += f"&c={ch}"
|
|
244
|
+
|
|
245
|
+
if self.trigger_channel is not None:
|
|
246
|
+
ch = self._ch_number(self.trigger_channel)
|
|
247
|
+
params += f"&C={ch}"
|
|
248
|
+
|
|
249
|
+
if self.trigger_mode is not None:
|
|
250
|
+
params += f"&M={self.trigger_mode.value}"
|
|
251
|
+
|
|
252
|
+
if self.trigger_level is not None:
|
|
253
|
+
params += f"&L={self.trigger_level}"
|
|
254
|
+
|
|
255
|
+
if self.trigger_timeout is not None:
|
|
256
|
+
params += f"&T={1000 * self.trigger_timeout}"
|
|
257
|
+
|
|
258
|
+
if self.pretrigger is not None:
|
|
259
|
+
params += f"&p={self.pretrigger}"
|
|
260
|
+
|
|
261
|
+
ret = self._dev.get(f"/capture?{params}", **kwargs)
|
|
262
|
+
if ret.get("error"):
|
|
263
|
+
raise CaptureError(ret.get("error"))
|
|
264
|
+
|
|
265
|
+
if ret["state"] == "available":
|
|
266
|
+
raise CaptureError("Failed to start capture.", ret["state"])
|
|
267
|
+
return ret["cookie"]
|
|
268
|
+
|
|
269
|
+
def result(
|
|
270
|
+
self, cookie: int, raw=False, **kwargs
|
|
271
|
+
) -> float | CaptureResult:
|
|
272
|
+
"""Return the result of the capture.
|
|
273
|
+
|
|
274
|
+
The method returns a number if the capture is still in
|
|
275
|
+
progress. If so, the number is from 0 and 1, giving the
|
|
276
|
+
fraction of the total amount of data captured so far. If all
|
|
277
|
+
the data has been captured, an object of class CaptureResult
|
|
278
|
+
is returned. If any error occurs, an exception is raised.
|
|
279
|
+
|
|
280
|
+
Required arguments:
|
|
281
|
+
|
|
282
|
+
cookie -- The cookie identifying the capture to return the
|
|
283
|
+
result for. This must be a value previously returned by a
|
|
284
|
+
call to Capture.start().
|
|
285
|
+
|
|
286
|
+
Keyword arguments:
|
|
287
|
+
|
|
288
|
+
raw -- If True, raw data is returned. Raw samples are numeric
|
|
289
|
+
values as returned by the sampling hardware, not physical
|
|
290
|
+
quantities.
|
|
291
|
+
|
|
292
|
+
"""
|
|
293
|
+
params = f"n={cookie}"
|
|
294
|
+
|
|
295
|
+
if raw:
|
|
296
|
+
params += "&r=True"
|
|
297
|
+
|
|
298
|
+
ret = self._dev.get(f"/capture?{params}", **kwargs)
|
|
299
|
+
if ret.get("error"):
|
|
300
|
+
raise CaptureError(ret.get("error"))
|
|
301
|
+
|
|
302
|
+
if ret["state"] == "available":
|
|
303
|
+
raise CaptureError("Capture aborted.") # somebody interfered?
|
|
304
|
+
if ret["state"] == "armed":
|
|
305
|
+
return 0.0
|
|
306
|
+
if ret["state"] == "capturing":
|
|
307
|
+
return ret["count"] / ret["max_count"]
|
|
308
|
+
if ret["state"] != "full":
|
|
309
|
+
raise CaptureError("Unknown capture state.", ret["state"])
|
|
310
|
+
|
|
311
|
+
ch_mask = 0
|
|
312
|
+
for n, w in enumerate(ret["ch_mask"]):
|
|
313
|
+
ch_mask |= w << (n * 32)
|
|
314
|
+
|
|
315
|
+
channel_names = []
|
|
316
|
+
result = CaptureResult()
|
|
317
|
+
ch = 0
|
|
318
|
+
m = 1
|
|
319
|
+
while ch_mask != 0:
|
|
320
|
+
if (ch_mask & m) != 0:
|
|
321
|
+
ch_mask &= ~m
|
|
322
|
+
name = self._ch_name(ch)
|
|
323
|
+
channel_names.append(name)
|
|
324
|
+
if name in self.channels:
|
|
325
|
+
# this is data we're interested in:
|
|
326
|
+
result.samples[name] = ChannelSamples()
|
|
327
|
+
ch += 1
|
|
328
|
+
m <<= 1
|
|
329
|
+
|
|
330
|
+
result.unix_ts = Decimal(ret["first_sample"])
|
|
331
|
+
tick_period = 1 / ret["ts_freq"]
|
|
332
|
+
dt = None
|
|
333
|
+
ci = 0 # cycles through the channels
|
|
334
|
+
sample_count = 0
|
|
335
|
+
for r in ret["r"]:
|
|
336
|
+
name = channel_names[ci]
|
|
337
|
+
|
|
338
|
+
if dt is None:
|
|
339
|
+
dt = 0
|
|
340
|
+
else:
|
|
341
|
+
dt += r["t"] * tick_period
|
|
342
|
+
|
|
343
|
+
if r.get("trigger"):
|
|
344
|
+
# update t for the pretrigger data:
|
|
345
|
+
for cs in result.samples.values():
|
|
346
|
+
for i, _ in enumerate(cs.ts):
|
|
347
|
+
cs.ts[i] -= dt
|
|
348
|
+
dt = 0
|
|
349
|
+
|
|
350
|
+
result.trigger_point = TriggerPoint(
|
|
351
|
+
channel=name, index=sample_count
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
for d in r["d"]:
|
|
355
|
+
cs = result.samples.get(name, None)
|
|
356
|
+
ci += 1
|
|
357
|
+
if ci >= len(channel_names):
|
|
358
|
+
ci = 0
|
|
359
|
+
sample_count += 1
|
|
360
|
+
name = channel_names[ci]
|
|
361
|
+
if cs is None:
|
|
362
|
+
continue # we're not interested in this channel
|
|
363
|
+
cs.ts.append(dt)
|
|
364
|
+
cs.ys.append(d)
|
|
365
|
+
return result
|
|
366
|
+
|
|
367
|
+
def acquire(
|
|
368
|
+
self, duration: float | None = None, **kwargs
|
|
369
|
+
) -> CaptureResult:
|
|
370
|
+
"""Synchronously capture waveform data.
|
|
371
|
+
|
|
372
|
+
This is a convenience method which first calls
|
|
373
|
+
Capture.start(duration) and then repeatedly calls
|
|
374
|
+
Capture.result() until all the data has been received. The
|
|
375
|
+
method sleeps for 100ms before repeating a call to
|
|
376
|
+
Capture.result(). The method returns the captured data if
|
|
377
|
+
successful or raises CaptureError otherwise.
|
|
378
|
+
|
|
379
|
+
Keyword arguments:
|
|
380
|
+
|
|
381
|
+
duration -- The maximum duration in seconds for which samples
|
|
382
|
+
should be captured. If left unspecified, samples are
|
|
383
|
+
captured until the meter's sampling buffer is full.
|
|
384
|
+
|
|
385
|
+
Additional keyword arguments are passed on to requests.get().
|
|
386
|
+
|
|
387
|
+
"""
|
|
388
|
+
cookie = self.start(duration, **kwargs)
|
|
389
|
+
while True:
|
|
390
|
+
time.sleep(100e-3)
|
|
391
|
+
result = self.result(cookie)
|
|
392
|
+
if isinstance(result, CaptureResult):
|
|
393
|
+
return result
|
|
394
|
+
|
|
395
|
+
def reset(self, cookie: int | None = None, **kwargs):
|
|
396
|
+
"""Cancel a pending capture and reset the capture service.
|
|
397
|
+
|
|
398
|
+
This resets the meter's state so it is available for a new
|
|
399
|
+
capture again.
|
|
400
|
+
|
|
401
|
+
This method raises a CaptureError if the reset fails for any
|
|
402
|
+
reason.
|
|
403
|
+
|
|
404
|
+
Keyword arguments:
|
|
405
|
+
|
|
406
|
+
cookie -- If specified, only the capture identified by the
|
|
407
|
+
cookie is canceled (assuming it is still pending).
|
|
408
|
+
|
|
409
|
+
Additional keyword arguments are passed along to the
|
|
410
|
+
requests.get() call which cancels the capture.
|
|
411
|
+
|
|
412
|
+
"""
|
|
413
|
+
params = "R"
|
|
414
|
+
if cookie is not None:
|
|
415
|
+
params = f"n={cookie}"
|
|
416
|
+
ret = self._dev.get(f"/capture?{params}", **kwargs)
|
|
417
|
+
if ret.get("error"):
|
|
418
|
+
raise CaptureError(ret.get("error"))
|
|
419
|
+
|
|
420
|
+
state = ret.get("state")
|
|
421
|
+
if state != "available":
|
|
422
|
+
raise CaptureError("Unexpected capture state.", state)
|
|
423
|
+
|
|
424
|
+
def _ch_number(self, name: str) -> int:
|
|
425
|
+
"""Get the channel number of a channel.
|
|
426
|
+
|
|
427
|
+
This raises a capture error if the channel is unknown.
|
|
428
|
+
|
|
429
|
+
Required arguments:
|
|
430
|
+
|
|
431
|
+
name -- The name of the channel whose number to return.
|
|
432
|
+
|
|
433
|
+
"""
|
|
434
|
+
cinfo = self._dev.channel_info().get(name)
|
|
435
|
+
if cinfo is None:
|
|
436
|
+
raise CaptureError("Unknown channel.", name)
|
|
437
|
+
return cinfo.chan
|
|
438
|
+
|
|
439
|
+
def _ch_name(self, ch: int) -> str | None:
|
|
440
|
+
"""Get the channel name for a channel number.
|
|
441
|
+
|
|
442
|
+
This returns None if the channel number is unknown.
|
|
443
|
+
|
|
444
|
+
Required arguments:
|
|
445
|
+
|
|
446
|
+
ch -- The number of the channel whose name to return.
|
|
447
|
+
|
|
448
|
+
"""
|
|
449
|
+
if self._ch_map is None:
|
|
450
|
+
self._ch_map = {}
|
|
451
|
+
for name, cinfo in self._dev.channel_info().items():
|
|
452
|
+
self._ch_map[int(cinfo.chan)] = name
|
|
453
|
+
return self._ch_map.get(ch)
|