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
egauge/ctid/waveform.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2016-2017, 2019-2022, 2024-2025 eGauge Systems LLC
|
|
3
|
+
# 4805 Sterling Dr, Suite 1
|
|
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
|
+
import math
|
|
31
|
+
|
|
32
|
+
from egauge.loggers import ModuleLogger
|
|
33
|
+
|
|
34
|
+
log = ModuleLogger.get(__name__)
|
|
35
|
+
|
|
36
|
+
_START_SYM_ZC_COUNT = 16 # number of zero-crossings in start-symbol
|
|
37
|
+
_CLK_FREQ = 480 # nominal clock frequency of CTid carrier
|
|
38
|
+
_CLK_TOL = 50 # clock tolerance in %
|
|
39
|
+
|
|
40
|
+
_MAX_CLK_FREQ = math.ceil(_CLK_FREQ * (1.0 + (_CLK_TOL / 100)))
|
|
41
|
+
_MIN_CLK_FREQ = math.floor(_CLK_FREQ * (1.0 - (_CLK_TOL / 100)))
|
|
42
|
+
_MIN_CLK_PERIOD = 1e6 / _MAX_CLK_FREQ # min. period in microseconds
|
|
43
|
+
_MAX_CLK_PERIOD = 1e6 / _MIN_CLK_FREQ # max. period in microseconds
|
|
44
|
+
|
|
45
|
+
_START_SYM_MAX_DURATION = _START_SYM_ZC_COUNT / 2 * _MAX_CLK_PERIOD
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def time_diff(l: float, r: float) -> float:
|
|
49
|
+
return l - r
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_zero_crossing(curr: float, prev: float, mean: float) -> bool:
|
|
53
|
+
return (prev < mean) != (curr < mean)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Window:
|
|
57
|
+
def __init__(self, sampling_freq: float):
|
|
58
|
+
self.sample_period = 1e6 / sampling_freq # period in microseconds
|
|
59
|
+
self.avg_len = math.ceil(_START_SYM_MAX_DURATION / self.sample_period)
|
|
60
|
+
window_len = 2 * self.avg_len
|
|
61
|
+
|
|
62
|
+
self.tolerance = 0.0
|
|
63
|
+
self.count = 0 # number of valid samples
|
|
64
|
+
self.avg_count = 0 # number of averaged samples
|
|
65
|
+
self.sum = 0 # sum of the valid samples
|
|
66
|
+
self.mean = 0 # mean value of valid samples
|
|
67
|
+
self.wr = 0 # index of next sample to be written
|
|
68
|
+
self.prev_polarity = 0 # previous polarity
|
|
69
|
+
self.ts: list[float] = [0] * window_len
|
|
70
|
+
self.val: list[float] = [0] * window_len
|
|
71
|
+
|
|
72
|
+
def enter_sample(self, ts: float, val: float):
|
|
73
|
+
if self.avg_count < self.avg_len:
|
|
74
|
+
self.avg_count += 1
|
|
75
|
+
else:
|
|
76
|
+
idx = self.wr + self.avg_len
|
|
77
|
+
if idx >= len(self.val):
|
|
78
|
+
idx -= len(self.val)
|
|
79
|
+
self.sum -= self.val[idx]
|
|
80
|
+
if self.count < len(self.val):
|
|
81
|
+
self.count += 1
|
|
82
|
+
self.sum += val
|
|
83
|
+
self.val[self.wr] = val
|
|
84
|
+
self.ts[self.wr] = ts
|
|
85
|
+
self.wr = (self.wr + 1) % len(self.val)
|
|
86
|
+
self.mean = self.sum / self.avg_count
|
|
87
|
+
|
|
88
|
+
def at_zero_crossing(self) -> bool:
|
|
89
|
+
curr_val = self.val[(self.wr + len(self.val) - 1) % len(self.val)]
|
|
90
|
+
curr_pol = curr_val >= self.mean
|
|
91
|
+
prev_pol = self.prev_polarity
|
|
92
|
+
self.prev_polarity = curr_pol
|
|
93
|
+
return curr_pol != prev_pol
|
|
94
|
+
|
|
95
|
+
def at_start_symbol(self) -> float | None:
|
|
96
|
+
if self.count < 1:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
curr = (self.wr + len(self.val) - 1) % len(self.val)
|
|
100
|
+
prev_val = self.val[curr]
|
|
101
|
+
self.prev_polarity = prev_val >= self.mean
|
|
102
|
+
end_ts = self.ts[curr]
|
|
103
|
+
start_ts = end_ts - _START_SYM_MAX_DURATION
|
|
104
|
+
total = 0
|
|
105
|
+
avg_count = 0
|
|
106
|
+
|
|
107
|
+
# See if we can find 16 zero-crossings within _START_SYM_MAX_DURATION:
|
|
108
|
+
first_ts = 0
|
|
109
|
+
zc_ts = []
|
|
110
|
+
i = 1
|
|
111
|
+
while i < self.count:
|
|
112
|
+
curr = (curr + len(self.val) - 1) % len(self.val)
|
|
113
|
+
curr_ts = self.ts[curr]
|
|
114
|
+
curr_val = self.val[curr]
|
|
115
|
+
|
|
116
|
+
if time_diff(start_ts, curr_ts) > _MAX_CLK_PERIOD:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
total += curr_val
|
|
120
|
+
avg_count += 1
|
|
121
|
+
|
|
122
|
+
if not is_zero_crossing(curr_val, prev_val, self.mean):
|
|
123
|
+
continue # keep looking for zero-crossing
|
|
124
|
+
prev_val = curr_val
|
|
125
|
+
zc_ts.append(curr_ts)
|
|
126
|
+
if len(zc_ts) >= _START_SYM_ZC_COUNT:
|
|
127
|
+
first_ts = curr_ts
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
i += 1
|
|
131
|
+
|
|
132
|
+
if len(zc_ts) < _START_SYM_ZC_COUNT:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
mean = total / avg_count
|
|
136
|
+
|
|
137
|
+
# Verify that there were no zero-crossings for
|
|
138
|
+
# _START_SYM_MAX_DURATION before the first edge of the
|
|
139
|
+
# start-symbol
|
|
140
|
+
while i < self.count:
|
|
141
|
+
curr = (curr + len(self.val) - 1) % len(self.val)
|
|
142
|
+
curr_ts = self.ts[curr]
|
|
143
|
+
curr_val = self.val[curr]
|
|
144
|
+
|
|
145
|
+
if is_zero_crossing(curr_val, prev_val, mean):
|
|
146
|
+
return None
|
|
147
|
+
if time_diff(first_ts, curr_ts) >= _START_SYM_MAX_DURATION:
|
|
148
|
+
break
|
|
149
|
+
prev_val = curr_val
|
|
150
|
+
|
|
151
|
+
i += 1
|
|
152
|
+
|
|
153
|
+
# Calculate average period based on the 7 full bit times the
|
|
154
|
+
# 16 zero-crossings cover (clock periods may be asymmetric):
|
|
155
|
+
period = time_diff(zc_ts[1], zc_ts[_START_SYM_ZC_COUNT - 1]) / (
|
|
156
|
+
_START_SYM_ZC_COUNT / 2 - 1
|
|
157
|
+
)
|
|
158
|
+
if period < _MIN_CLK_PERIOD or period > _MAX_CLK_PERIOD:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
self.tolerance = max(period / 4, 1.2 * self.sample_period)
|
|
162
|
+
|
|
163
|
+
# Verify that each bit has the expected period:
|
|
164
|
+
curr_ts = end_ts
|
|
165
|
+
for i in range(0, _START_SYM_ZC_COUNT, 2):
|
|
166
|
+
dt = time_diff(curr_ts, zc_ts[i + 1])
|
|
167
|
+
if abs(dt - period) > self.tolerance:
|
|
168
|
+
return None
|
|
169
|
+
curr_ts = zc_ts[i + 1]
|
|
170
|
+
|
|
171
|
+
# Now that we confirmed a start-symbol, commit to its mean value:
|
|
172
|
+
self.sum = total
|
|
173
|
+
self.avg_count = avg_count
|
|
174
|
+
self.mean = mean
|
|
175
|
+
return period
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class ByteDecoder:
|
|
179
|
+
def __init__(self):
|
|
180
|
+
self.num_bits = 0
|
|
181
|
+
self.val = 0
|
|
182
|
+
self.edge_count = 0
|
|
183
|
+
self.run_length = 0
|
|
184
|
+
self.start_ts = None
|
|
185
|
+
self.decoded_byte = 0
|
|
186
|
+
|
|
187
|
+
def reset(self):
|
|
188
|
+
self.__init__()
|
|
189
|
+
|
|
190
|
+
def timed_out(self, period: float, now: float) -> bool:
|
|
191
|
+
if self.start_ts is None:
|
|
192
|
+
return False
|
|
193
|
+
return time_diff(now, self.start_ts) > 4 * period
|
|
194
|
+
|
|
195
|
+
def update(self, period: float, tolerance: float, ts: float) -> bool:
|
|
196
|
+
if self.start_ts is None:
|
|
197
|
+
self.start_ts = ts
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
if time_diff(ts, self.start_ts + period - tolerance) < 0:
|
|
201
|
+
# got what looks like a 1-edge
|
|
202
|
+
self.edge_count += 1
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
bit = self.edge_count & 1
|
|
206
|
+
log.debug(
|
|
207
|
+
"bit %d at 0x%08x (edge_count %d, start_ts 0x%08x)",
|
|
208
|
+
bit,
|
|
209
|
+
ts,
|
|
210
|
+
self.edge_count,
|
|
211
|
+
self.start_ts,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
self.edge_count = 0
|
|
215
|
+
self.start_ts = ts
|
|
216
|
+
if self.run_length >= 7:
|
|
217
|
+
# drop stuffer bit...
|
|
218
|
+
self.run_length = 0
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
if bit:
|
|
222
|
+
self.run_length += 1
|
|
223
|
+
else:
|
|
224
|
+
self.run_length = 0
|
|
225
|
+
|
|
226
|
+
self.val = (self.val << 1) | bit
|
|
227
|
+
self.num_bits += 1
|
|
228
|
+
|
|
229
|
+
if self.num_bits < 8:
|
|
230
|
+
return False
|
|
231
|
+
self.decoded_byte = self.val
|
|
232
|
+
self.val = 0
|
|
233
|
+
self.num_bits = 0
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
def get_byte(self) -> int:
|
|
237
|
+
return self.decoded_byte
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class Decoder:
|
|
241
|
+
"""Decode a sequence of equidistant sample values that represent a
|
|
242
|
+
differential Manchester-encoded signal into a byte stream.
|
|
243
|
+
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def __init__(self, sampling_freq: float):
|
|
247
|
+
"""Create a waveform decoder for data sampled at a given
|
|
248
|
+
frequency.
|
|
249
|
+
|
|
250
|
+
Required arguments:
|
|
251
|
+
|
|
252
|
+
sampling_freq -- The sampling frequency in Hertz.
|
|
253
|
+
|
|
254
|
+
"""
|
|
255
|
+
self.w = Window(sampling_freq)
|
|
256
|
+
self.bd = ByteDecoder()
|
|
257
|
+
self.period = None
|
|
258
|
+
|
|
259
|
+
def add_sample(self, timestamp: float, value: float) -> int:
|
|
260
|
+
"""Enter a sample value for a given timestamp.
|
|
261
|
+
|
|
262
|
+
Returns -1 when a new start-symbol was detected, 0 when more
|
|
263
|
+
data is needed, and 1 if a complete byte has been decoded.
|
|
264
|
+
|
|
265
|
+
Required arguments:
|
|
266
|
+
|
|
267
|
+
timestamp -- The time in microseconds at which the sample
|
|
268
|
+
value was acquired.
|
|
269
|
+
|
|
270
|
+
value -- The value of the sample.
|
|
271
|
+
|
|
272
|
+
"""
|
|
273
|
+
self.w.enter_sample(timestamp, value)
|
|
274
|
+
|
|
275
|
+
if self.period is None:
|
|
276
|
+
self.period = self.w.at_start_symbol()
|
|
277
|
+
if self.period is None:
|
|
278
|
+
return 0
|
|
279
|
+
self.bd.reset()
|
|
280
|
+
return -1 # got a new start-symbol
|
|
281
|
+
|
|
282
|
+
if self.bd.timed_out(self.period, timestamp):
|
|
283
|
+
# check if implied last edge finishes a byte, if so, return it:
|
|
284
|
+
ret = self.bd.update(self.period, self.w.tolerance, timestamp)
|
|
285
|
+
self.period = None
|
|
286
|
+
return 1 if ret else 0
|
|
287
|
+
|
|
288
|
+
if not self.w.at_zero_crossing():
|
|
289
|
+
return 0
|
|
290
|
+
|
|
291
|
+
log.debug("zero-crossing at 0x%08x (mean %d)", timestamp, self.w.mean)
|
|
292
|
+
return self.bd.update(self.period, self.w.tolerance, timestamp)
|
|
293
|
+
|
|
294
|
+
def get_byte(self) -> int:
|
|
295
|
+
"""After Decoder.add_samples() returns 1, this returns the
|
|
296
|
+
most recently decoded byte.
|
|
297
|
+
|
|
298
|
+
"""
|
|
299
|
+
return self.bd.get_byte()
|
|
Binary file
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2023-2024 eGauge Systems LLC
|
|
4
|
+
#
|
|
5
|
+
# See LICENSE file for details.
|
|
6
|
+
#
|
|
7
|
+
"""This test program demonstrates the use of class
|
|
8
|
+
egauge.webapi.device.Capture to capture waveform data via the /capture
|
|
9
|
+
WebAPI. When executed, it captures 34ms of waveform data for up to
|
|
10
|
+
the first three channels, then creates a timeplot of the data using
|
|
11
|
+
matplotlib.
|
|
12
|
+
|
|
13
|
+
Install egauge-python with a command of the form:
|
|
14
|
+
|
|
15
|
+
pip install egauge-python[examples]
|
|
16
|
+
|
|
17
|
+
to ensure that matplotlib is installed on your system.
|
|
18
|
+
|
|
19
|
+
You can set environment variables:
|
|
20
|
+
|
|
21
|
+
EGDEV - the URL of the meter to use (e.g., http://eGaugeXXX.local)
|
|
22
|
+
EGUSR - the username to log in to the meter (e.g., "owner")
|
|
23
|
+
EGPWD - the password for the username
|
|
24
|
+
|
|
25
|
+
Alternatively, you can edit examples/test_common.py to suit your needs.
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
from matplotlib import pyplot
|
|
32
|
+
|
|
33
|
+
from egauge.examples import test_common
|
|
34
|
+
from egauge.webapi.device import Capture, TriggerMode
|
|
35
|
+
|
|
36
|
+
cap = Capture(test_common.dev)
|
|
37
|
+
print(f"available channels: {cap.available_channels}")
|
|
38
|
+
|
|
39
|
+
# capture samples for (up to) the first three channels:
|
|
40
|
+
if len(cap.available_channels) < 1:
|
|
41
|
+
print(
|
|
42
|
+
"The meter has no channels configured - waveform cannot be acquired."
|
|
43
|
+
)
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
elif len(cap.available_channels) > 3:
|
|
46
|
+
cap.channels = set(cap.available_channels[0:3])
|
|
47
|
+
else:
|
|
48
|
+
cap.channels = set(cap.available_channels)
|
|
49
|
+
|
|
50
|
+
# these settings are only needed for triggered captures:
|
|
51
|
+
cap.trigger_mode = TriggerMode.RISING
|
|
52
|
+
cap.trigger_channel = "L1"
|
|
53
|
+
cap.trigger_level = 0
|
|
54
|
+
cap.pretrigger = 0.0083 # how many seconds of data to keep ahead of trigger
|
|
55
|
+
cap.trigger_timeout = 1 # number of seconds before auto triggering
|
|
56
|
+
|
|
57
|
+
data = cap.acquire(duration=0.034)
|
|
58
|
+
|
|
59
|
+
# show the trigger point sample (if any):
|
|
60
|
+
if data.trigger_point is None:
|
|
61
|
+
print("Capture was auto-triggered.")
|
|
62
|
+
else:
|
|
63
|
+
print(data.trigger_point)
|
|
64
|
+
|
|
65
|
+
# plot the data:
|
|
66
|
+
_, axis = pyplot.subplots()
|
|
67
|
+
for chan in cap.channels:
|
|
68
|
+
axis.plot(data.samples[chan].ts, data.samples[chan].ys, label=chan)
|
|
69
|
+
axis.set(
|
|
70
|
+
xlabel="time [s]",
|
|
71
|
+
ylabel="[" + cap.channel_unit(chan) + "]",
|
|
72
|
+
title="captured waveform data",
|
|
73
|
+
)
|
|
74
|
+
axis.grid()
|
|
75
|
+
pyplot.legend(loc="upper right")
|
|
76
|
+
print("Plotting the waveform - close the plot window when done")
|
|
77
|
+
pyplot.show()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from egauge import webapi
|
|
5
|
+
|
|
6
|
+
#
|
|
7
|
+
# You can edit the values below directly or use environment variables
|
|
8
|
+
# EGDEV, EGUSR, and EGPWD to set the device URL, username, and
|
|
9
|
+
# password, respectively.
|
|
10
|
+
#
|
|
11
|
+
meter_dev = os.getenv("EGDEV", "http://egauge-dut")
|
|
12
|
+
meter_user = os.getenv("EGUSR", "dmo")
|
|
13
|
+
meter_password = os.getenv("EGPWD", "secret password")
|
|
14
|
+
|
|
15
|
+
dev = webapi.device.Device(
|
|
16
|
+
meter_dev, webapi.JWTAuth(meter_user, meter_password)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# verify we can talk to the meter:
|
|
20
|
+
try:
|
|
21
|
+
rights = dev.get("/auth/rights").get("rights", [])
|
|
22
|
+
except webapi.Error as e:
|
|
23
|
+
print(f"Sorry, failed to connect to {meter_dev}: {e}")
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
|
|
26
|
+
print(f"Using meter {meter_dev} (user {meter_user}, rights={rights})")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2023-2024 eGauge Systems LLC
|
|
4
|
+
#
|
|
5
|
+
# See LICENSE file for details.
|
|
6
|
+
#
|
|
7
|
+
"""This test program demonstrates the use of class
|
|
8
|
+
egauge.webapi.device.CTidInfo to scan the information from a
|
|
9
|
+
CTid®-enabled sensor and/or to blink the locator LED on such a sensor.
|
|
10
|
+
|
|
11
|
+
Install egauge-python with a command of the form:
|
|
12
|
+
|
|
13
|
+
pip install egauge-python[examples]
|
|
14
|
+
|
|
15
|
+
to ensure that the readchar module is installed on your system.
|
|
16
|
+
|
|
17
|
+
You can set environment variables:
|
|
18
|
+
|
|
19
|
+
EGDEV - the URL of the meter to use (e.g., http://eGaugeXXX.local)
|
|
20
|
+
EGUSR - the username to log in to the meter (e.g., "owner")
|
|
21
|
+
EGPWD - the password for the username
|
|
22
|
+
|
|
23
|
+
Alternatively, you can edit examples/test_common.py to suit your needs.
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
import readchar
|
|
30
|
+
|
|
31
|
+
from egauge.examples import test_common
|
|
32
|
+
from egauge.webapi.device import CTidInfo, PortInfo
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ask_port():
|
|
36
|
+
result = input("Port number (1-30): ")
|
|
37
|
+
if result.startswith("q"):
|
|
38
|
+
sys.exit(0)
|
|
39
|
+
return int(result)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def print_port_info(port_info):
|
|
43
|
+
port = port_info.port
|
|
44
|
+
mfg = port_info.short_mfg_name()
|
|
45
|
+
model = port_info.model_name()
|
|
46
|
+
print(f"\tS{port}: {mfg} {model}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
ctid_info = CTidInfo(test_common.dev)
|
|
50
|
+
print("ports with existing CTid® info:")
|
|
51
|
+
|
|
52
|
+
for port_info in ctid_info:
|
|
53
|
+
print_port_info(port_info)
|
|
54
|
+
|
|
55
|
+
port = None
|
|
56
|
+
|
|
57
|
+
while True:
|
|
58
|
+
while port is None:
|
|
59
|
+
port = ask_port()
|
|
60
|
+
sys.stdout.write(
|
|
61
|
+
"Press b (blink), s (scan), c (change port), d (delete), q (quit): "
|
|
62
|
+
)
|
|
63
|
+
sys.stdout.flush()
|
|
64
|
+
ch = readchar.readchar()
|
|
65
|
+
print()
|
|
66
|
+
if ch == "s":
|
|
67
|
+
print(f" scanning port S{port}...")
|
|
68
|
+
result = ctid_info.scan(port)
|
|
69
|
+
if isinstance(result, PortInfo):
|
|
70
|
+
if result.table:
|
|
71
|
+
print(" detected sensor:")
|
|
72
|
+
print_port_info(result)
|
|
73
|
+
else:
|
|
74
|
+
print(" no CTid® sensor detected")
|
|
75
|
+
else:
|
|
76
|
+
print(f" scan of port S{port} failed: {result}")
|
|
77
|
+
elif ch == "b":
|
|
78
|
+
print(f" blinking port S{port}...")
|
|
79
|
+
ctid_info.flash(port)
|
|
80
|
+
elif ch == "c":
|
|
81
|
+
port = None
|
|
82
|
+
elif ch == "d":
|
|
83
|
+
print(f" deleting info saved for S{port}...")
|
|
84
|
+
ctid_info.delete(port)
|
|
85
|
+
elif ch == "q":
|
|
86
|
+
print("Done.")
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
ctid_info.stop()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2016-2017, 2024 eGauge Systems LLC
|
|
4
|
+
#
|
|
5
|
+
# See LICENSE file for details.
|
|
6
|
+
#
|
|
7
|
+
"""This program is a simple demonstration of how the ctid.Decoder()
|
|
8
|
+
can be used to decode the CTid info from a waveform consisting of a
|
|
9
|
+
series of timestamped samples. The data is read from
|
|
10
|
+
`examples/data/test-ctid-decoder.raw`, which contains data sampled at
|
|
11
|
+
8kHz.
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import pickle
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from egauge import ctid
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def fmt_ts(timestamp: float) -> str:
|
|
23
|
+
"""Format a timestamp in a human-readable format.
|
|
24
|
+
|
|
25
|
+
Required arguments:
|
|
26
|
+
|
|
27
|
+
timestamp -- A timestamp measured in microseconds.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
sec = 1e-6 * timestamp
|
|
31
|
+
return f"{sec:.3f} sec"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def decode(
|
|
35
|
+
sample_rate: float, ts_list: list[float], values: list[float]
|
|
36
|
+
) -> ctid.Table | None:
|
|
37
|
+
"""Decode a series of samples and convert them to a CTid table.
|
|
38
|
+
|
|
39
|
+
Returns a ctid.Table object as soon as the first CTid table has
|
|
40
|
+
been successfully decoded or None if no CTid table was found.
|
|
41
|
+
|
|
42
|
+
Required arguments:
|
|
43
|
+
|
|
44
|
+
sampling_rate -- The average frequency at which the samples were
|
|
45
|
+
acquired. This is used to properly size the decoding window
|
|
46
|
+
size. Assuming a sufficient number of samples, this could be
|
|
47
|
+
inferred from `ts_list`.
|
|
48
|
+
|
|
49
|
+
ts_list -- The list of timestamps, measured in microseconds.
|
|
50
|
+
|
|
51
|
+
values -- The list of sample values. This must be the same length
|
|
52
|
+
as `ts_list`.
|
|
53
|
+
|
|
54
|
+
"""
|
|
55
|
+
table_data = b""
|
|
56
|
+
decoder = ctid.Decoder(sample_rate)
|
|
57
|
+
for i, val in enumerate(values):
|
|
58
|
+
ts = ts_list[i]
|
|
59
|
+
ret = decoder.add_sample(ts, val)
|
|
60
|
+
if ret < 0:
|
|
61
|
+
print(f"found start symbol at {fmt_ts(ts)}")
|
|
62
|
+
table_data = b""
|
|
63
|
+
elif ret > 0:
|
|
64
|
+
byte = decoder.get_byte()
|
|
65
|
+
idx = len(table_data)
|
|
66
|
+
print(f"decoded byte {idx:2}: 0x{byte:02x} at {fmt_ts(ts)}")
|
|
67
|
+
table_data += byte.to_bytes(1)
|
|
68
|
+
try:
|
|
69
|
+
table = ctid.Table(table_data)
|
|
70
|
+
return table
|
|
71
|
+
except ctid.CRCError as e:
|
|
72
|
+
print(f"Error: {e}")
|
|
73
|
+
decoder = ctid.Decoder(sample_rate)
|
|
74
|
+
table_data = b""
|
|
75
|
+
except ctid.Error:
|
|
76
|
+
pass # need more data
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
input_path = Path(__file__).parent / "data" / "test-ctid-decoder.raw"
|
|
81
|
+
|
|
82
|
+
with open(input_path, "rb") as f:
|
|
83
|
+
(freq, ts, samples) = pickle.load(f)
|
|
84
|
+
|
|
85
|
+
print(f"decoding data (sampling rate {freq:.1f} Hz)...")
|
|
86
|
+
|
|
87
|
+
table = decode(freq, ts, samples)
|
|
88
|
+
|
|
89
|
+
if table is None:
|
|
90
|
+
print("Sorry, no CTid table detected...", file=sys.stderr)
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
print(f"Received: {table}")
|