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/ctid.py
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2016-2017, 2019-2025 eGauge Systems LLC
|
|
4
|
+
# 4805 Sterling Dr, Suite 1
|
|
5
|
+
# Boulder, CO 80301
|
|
6
|
+
# voice: 720-545-9767
|
|
7
|
+
# email: davidm@egauge.net
|
|
8
|
+
#
|
|
9
|
+
# All rights reserved.
|
|
10
|
+
#
|
|
11
|
+
# MIT License
|
|
12
|
+
#
|
|
13
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
# in the Software without restriction, including without limitation the rights
|
|
16
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
# furnished to do so, subject to the following conditions:
|
|
19
|
+
#
|
|
20
|
+
# The above copyright notice and this permission notice shall be included in
|
|
21
|
+
# all copies or substantial portions of the Software.
|
|
22
|
+
#
|
|
23
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
29
|
+
# THE SOFTWARE.
|
|
30
|
+
#
|
|
31
|
+
import math
|
|
32
|
+
import struct
|
|
33
|
+
|
|
34
|
+
import crcmod
|
|
35
|
+
from deprecated import deprecated
|
|
36
|
+
|
|
37
|
+
from .bit_stuffer import BitStuffer
|
|
38
|
+
|
|
39
|
+
CTID_VERSION = 5 # latest version of CTid Specification this code supports
|
|
40
|
+
START_SYM = 0xFF
|
|
41
|
+
|
|
42
|
+
SENSOR_TYPE_AC = 0x0 # AC-only sensor
|
|
43
|
+
SENSOR_TYPE_DC = 0x1 # DC-capable sensor
|
|
44
|
+
SENSOR_TYPE_RC = 0x2 # differential-output sensor ("Rogowski Coil"...)
|
|
45
|
+
SENSOR_TYPE_VOLTAGE = 0x3 # DEPRECATED -- use SENSOR_TYPE_LINEAR instead
|
|
46
|
+
SENSOR_TYPE_LINEAR = 0x3
|
|
47
|
+
SENSOR_TYPE_TEMP_LINEAR = 0x4
|
|
48
|
+
SENSOR_TYPE_TEMP_NTC = 0x5
|
|
49
|
+
SENSOR_TYPE_PULSE = 0x6
|
|
50
|
+
|
|
51
|
+
# List of registered manufacturer IDs:
|
|
52
|
+
_MFG_ID = {
|
|
53
|
+
0x0000: ["eGauge Systems LLC", "eGauge"],
|
|
54
|
+
0x0001: ["Magnelab, Inc.", "Magnelab"],
|
|
55
|
+
0x0002: ["Continental Control Systems LLC", "CCS"],
|
|
56
|
+
0x0003: ["J&D Electronics", "J&D"],
|
|
57
|
+
0x0004: ["Accuenergy, Inc.", "Accuenergy"],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# For backwards-compatibility:
|
|
61
|
+
MFG_ID = {}
|
|
62
|
+
for key, value in _MFG_ID.items():
|
|
63
|
+
MFG_ID[key] = value[0]
|
|
64
|
+
|
|
65
|
+
SENSOR_TYPE_NAME = ["AC", "DC", "RC", "linear", "temp", "NTC", "pulse"]
|
|
66
|
+
|
|
67
|
+
SENSOR_UNITS = (
|
|
68
|
+
("V", "voltage"),
|
|
69
|
+
("A", "current"),
|
|
70
|
+
("Ah", "charge"),
|
|
71
|
+
("W", "power"),
|
|
72
|
+
("var", "reactive power"),
|
|
73
|
+
("VA", "apparent power"),
|
|
74
|
+
("Hz", "frequency"),
|
|
75
|
+
("Ω", "resistance"),
|
|
76
|
+
("°", "angle"),
|
|
77
|
+
("°C", "temperature"),
|
|
78
|
+
("%RH", "humidity"),
|
|
79
|
+
("Pa", "pressure"),
|
|
80
|
+
("g", "mass"),
|
|
81
|
+
("m/s", "velocity"),
|
|
82
|
+
("g/s", "mass flow"),
|
|
83
|
+
("m^3/s", "volumetric flow"),
|
|
84
|
+
("%", "percentage"),
|
|
85
|
+
("ppm", "parts-per-million"),
|
|
86
|
+
("", "air quality"),
|
|
87
|
+
("", "number"),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
crc8_rohc = crcmod.predefined.mkCrcFun("crc-8-rohc")
|
|
91
|
+
crc16_modbus = crcmod.predefined.mkCrcFun("modbus")
|
|
92
|
+
|
|
93
|
+
# Max. mantissa value for float12 format:
|
|
94
|
+
F12_MAX_MANTISSA = 0x1FF
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class CRCError(Exception):
|
|
98
|
+
"""Exception raised in case of a CRC error. Property `expected`
|
|
99
|
+
holds the expected CRC value, `got` holds the received CRC.
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, expected, got):
|
|
104
|
+
super().__init__()
|
|
105
|
+
self.expected = expected
|
|
106
|
+
self.got = got
|
|
107
|
+
|
|
108
|
+
def __str__(self):
|
|
109
|
+
return f"Expected CRC {self.expected:#x} but got {self.got:#x}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Error(Exception):
|
|
113
|
+
"""This is raised for errors other than CRCErrors."""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_mfg_id_for_name(name: str) -> int:
|
|
117
|
+
"""Get the manufacturer id for the given name.
|
|
118
|
+
|
|
119
|
+
Raises Error if the passed string is not unique or if the passed
|
|
120
|
+
string is neither a recognized name or a valid number.
|
|
121
|
+
|
|
122
|
+
Required arguments:
|
|
123
|
+
|
|
124
|
+
name -- The name of the manufacturer for which to return the id.
|
|
125
|
+
This may be a prefix of the full name, as long as the prefix
|
|
126
|
+
is unique. Alternatively, name may also be a decimal integer
|
|
127
|
+
string or a hexdecimal string with prefix 0x.
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
match = None
|
|
131
|
+
for key, value in MFG_ID.items():
|
|
132
|
+
if len(value) >= len(name):
|
|
133
|
+
if value.startswith(name):
|
|
134
|
+
if match is None:
|
|
135
|
+
match = key
|
|
136
|
+
else:
|
|
137
|
+
raise Error(
|
|
138
|
+
"Name is ambiguous. Please specify more letters."
|
|
139
|
+
)
|
|
140
|
+
if match is not None:
|
|
141
|
+
return match
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
return int(name, 0)
|
|
145
|
+
except ValueError as e:
|
|
146
|
+
raise Error("Neither a name nor a valid number.") from e
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_sensor_type_id(name_or_number: str) -> int:
|
|
150
|
+
"""Convert a string to a sensor type id.
|
|
151
|
+
|
|
152
|
+
Raises Error if the passed string is neither a recognized sensor
|
|
153
|
+
type name nor a valid number string.
|
|
154
|
+
|
|
155
|
+
Required arguments:
|
|
156
|
+
|
|
157
|
+
name_or_number -- The string to convert to a sensor type id. The
|
|
158
|
+
string may be one of the names listed in
|
|
159
|
+
`ctid.SENSOR_TYPE_NAME` (the comparison ignores case) or it
|
|
160
|
+
may be a number representing the integer id of the sensor
|
|
161
|
+
type. The number string should be in decimal, unless it
|
|
162
|
+
starts with "0x", in which case it must be in hexadecimal.
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
lc_name = name_or_number.lower()
|
|
166
|
+
for i, name in enumerate(SENSOR_TYPE_NAME):
|
|
167
|
+
if name.lower() == lc_name:
|
|
168
|
+
return i
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
return int(name_or_number, 0)
|
|
172
|
+
except ValueError as e:
|
|
173
|
+
raise Error("Neither a sensor type name nor a valid number.") from e
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def get_sensor_type_name(sensor_type: int) -> str | None:
|
|
177
|
+
if sensor_type < 0 or sensor_type >= len(SENSOR_TYPE_NAME):
|
|
178
|
+
return None
|
|
179
|
+
return SENSOR_TYPE_NAME[sensor_type]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def get_sensor_unit(unit_code: int) -> str:
|
|
183
|
+
if 0 <= unit_code < len(SENSOR_UNITS):
|
|
184
|
+
return SENSOR_UNITS[unit_code][0]
|
|
185
|
+
return "?"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_sensor_unit_desc(unit_code: int) -> str:
|
|
189
|
+
if 0 <= unit_code < len(SENSOR_UNITS):
|
|
190
|
+
return SENSOR_UNITS[unit_code][1]
|
|
191
|
+
return "?"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def mfg_name(ident: int) -> str | None:
|
|
195
|
+
"""Get the manufacturer name for a given manufacturer id.
|
|
196
|
+
|
|
197
|
+
Returns None if the specified id is unknown.
|
|
198
|
+
|
|
199
|
+
Required arguments:
|
|
200
|
+
|
|
201
|
+
ident -- The id of the manufacturer whose name to return.
|
|
202
|
+
|
|
203
|
+
"""
|
|
204
|
+
if ident in _MFG_ID:
|
|
205
|
+
return _MFG_ID[ident][0]
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def mfg_short_name(ident: int) -> str | None:
|
|
210
|
+
"""Get the short version of a manufacturer's name for a given
|
|
211
|
+
manufacturer id.
|
|
212
|
+
|
|
213
|
+
Returns None if the specified id is unknown.
|
|
214
|
+
|
|
215
|
+
Required arguments:
|
|
216
|
+
|
|
217
|
+
ident -- The id of the manufacturer whose short name to return.
|
|
218
|
+
|
|
219
|
+
"""
|
|
220
|
+
if ident in _MFG_ID:
|
|
221
|
+
return _MFG_ID[ident][1]
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@deprecated(version="0.0", reason="Use CTid.mfg_name() instead")
|
|
226
|
+
def get_mfg_id(ident: int):
|
|
227
|
+
return mfg_name(ident)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def s10(val: int) -> int:
|
|
231
|
+
"""Convert 10 bit unsigned integer to a signed 9-bit integer.
|
|
232
|
+
|
|
233
|
+
Required arguments:
|
|
234
|
+
|
|
235
|
+
val -- The integer to convert.
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
if not 0 <= val < 1024:
|
|
239
|
+
raise ValueError
|
|
240
|
+
if val < 512:
|
|
241
|
+
return val
|
|
242
|
+
return val - 1024
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def fix(
|
|
246
|
+
val: float,
|
|
247
|
+
num_bits: int,
|
|
248
|
+
is_signed: bool,
|
|
249
|
+
name: str,
|
|
250
|
+
unit: str | None,
|
|
251
|
+
scale: float,
|
|
252
|
+
):
|
|
253
|
+
f = round(val / scale)
|
|
254
|
+
limit = 1 << num_bits
|
|
255
|
+
min_val = -(limit / 2) if is_signed else 0
|
|
256
|
+
max_val = +(limit / 2 - 1) if is_signed else limit - 1
|
|
257
|
+
if f < min_val or f > max_val:
|
|
258
|
+
unit_str = (" " + unit) if unit is not None else ""
|
|
259
|
+
prec = -int(math.log10(scale))
|
|
260
|
+
val_str = f"{val:.{prec}f}"
|
|
261
|
+
min_str = f"{min_val * scale:.{prec}f}"
|
|
262
|
+
max_str = f"{max_val * scale:.{prec}f}"
|
|
263
|
+
raise Error(
|
|
264
|
+
f"{name} {val_str}{unit_str} outside of range "
|
|
265
|
+
f"from {min_str} to {max_str}{unit_str}."
|
|
266
|
+
)
|
|
267
|
+
return f
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def unfix(val: int, scale: float) -> float:
|
|
271
|
+
return val * scale
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def float12(val: float, name: str, unit: str) -> int:
|
|
275
|
+
"""Encode a floating point value in the range from 0 to 1e7 to the
|
|
276
|
+
12-bit float format.
|
|
277
|
+
|
|
278
|
+
Required arguments:
|
|
279
|
+
|
|
280
|
+
val -- The floating point value to convert.
|
|
281
|
+
|
|
282
|
+
name -- The name of the CTid parameters being converted.
|
|
283
|
+
|
|
284
|
+
unit -- The physical unit of the CTid parameter being converted.
|
|
285
|
+
"""
|
|
286
|
+
if val < 0 or val > 1e7:
|
|
287
|
+
raise Error(
|
|
288
|
+
f"{name} {val}{unit} outside of range from 0 to 10,000,000{unit}"
|
|
289
|
+
)
|
|
290
|
+
exp = math.log10(val) if val >= 1.0 else 0
|
|
291
|
+
exp = int(math.ceil(exp))
|
|
292
|
+
mantissa = int(round((val / math.pow(10, exp)) * F12_MAX_MANTISSA))
|
|
293
|
+
return (mantissa << 3) | exp
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def unfloat12(val: int) -> float:
|
|
297
|
+
"""Decode a 12-bit integer to a floating point value in the range
|
|
298
|
+
from 0 to 1e7.
|
|
299
|
+
|
|
300
|
+
Required arguments:
|
|
301
|
+
|
|
302
|
+
val -- The integer value to decode.
|
|
303
|
+
|
|
304
|
+
"""
|
|
305
|
+
mantissa = (val >> 3) & F12_MAX_MANTISSA
|
|
306
|
+
exp = val & 0x7
|
|
307
|
+
return mantissa / float(F12_MAX_MANTISSA) * math.pow(10, exp)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def decimal16(val: float, name: str, unit: str) -> int:
|
|
311
|
+
"""Encode a decimal value with a 11-bit signed mantissa and a
|
|
312
|
+
power-of-ten exponent from -16..15 as a 16-bit integer.
|
|
313
|
+
This covers the range from -1024e15 to +1023e15.
|
|
314
|
+
|
|
315
|
+
Required arguments:
|
|
316
|
+
|
|
317
|
+
val -- The floating point value to convert.
|
|
318
|
+
|
|
319
|
+
name -- The name of the CTid parameters being converted.
|
|
320
|
+
|
|
321
|
+
unit -- The physical unit of the CTid parameter being converted.
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
mantissa = 0
|
|
325
|
+
exp = 0
|
|
326
|
+
|
|
327
|
+
if val != 0.0:
|
|
328
|
+
# Determine exponent that gives us an mantissa in the range
|
|
329
|
+
# from -1024..-103 or 103..1023:
|
|
330
|
+
while exp > -16 and -102.4 <= val <= 102.3:
|
|
331
|
+
val *= 10.0
|
|
332
|
+
exp -= 1
|
|
333
|
+
mantissa = round(val)
|
|
334
|
+
while mantissa < -1024 or mantissa > 1023:
|
|
335
|
+
mantissa /= 10.0
|
|
336
|
+
exp += 1
|
|
337
|
+
if exp > 15:
|
|
338
|
+
raise Error(
|
|
339
|
+
f"{name} {val}{unit} outside of range "
|
|
340
|
+
f"from -1024e15..1023e15{unit}"
|
|
341
|
+
)
|
|
342
|
+
mantissa = int(mantissa)
|
|
343
|
+
return ((mantissa & 0x7FF) << 5) | (exp & 0x1F)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def undecimal16(val: int) -> float:
|
|
347
|
+
"""Decode 16-bit integer to a floating number number.
|
|
348
|
+
|
|
349
|
+
Required arguments:
|
|
350
|
+
|
|
351
|
+
val -- The integer value to decode.
|
|
352
|
+
|
|
353
|
+
"""
|
|
354
|
+
mantissa = (val >> 5) & 0x7FF
|
|
355
|
+
if mantissa >= 0x400:
|
|
356
|
+
mantissa -= 0x800
|
|
357
|
+
exp = val & 0x1F
|
|
358
|
+
if exp >= 0x10:
|
|
359
|
+
exp -= 0x20
|
|
360
|
+
return mantissa * math.pow(10, exp)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def check_table_data(raw_data: bytes):
|
|
364
|
+
version = struct.unpack_from(">B", raw_data)[0]
|
|
365
|
+
if version <= 1:
|
|
366
|
+
length = 30
|
|
367
|
+
if len(raw_data) < length + 1: # excluding start symbol
|
|
368
|
+
raise Error("Insufficient table data")
|
|
369
|
+
exp_crc = crc8_rohc(raw_data[:length])
|
|
370
|
+
got_crc = struct.unpack_from(">B", raw_data[length:])[0]
|
|
371
|
+
if exp_crc != got_crc:
|
|
372
|
+
raise CRCError(exp_crc, got_crc)
|
|
373
|
+
elif version <= 3:
|
|
374
|
+
length = 33
|
|
375
|
+
if len(raw_data) < length + 2: # excluding start symbol
|
|
376
|
+
raise Error("Insufficient table data")
|
|
377
|
+
exp_crc = crc16_modbus(raw_data[:length])
|
|
378
|
+
got_crc = struct.unpack_from(">H", raw_data[length:])[0]
|
|
379
|
+
if exp_crc != got_crc:
|
|
380
|
+
raise CRCError(exp_crc, got_crc)
|
|
381
|
+
else:
|
|
382
|
+
length = 43
|
|
383
|
+
if len(raw_data) < length + 2: # excluding start symbol
|
|
384
|
+
raise Error("Insufficient table data")
|
|
385
|
+
exp_crc = crc16_modbus(raw_data[:length])
|
|
386
|
+
got_crc = struct.unpack_from(">H", raw_data[length:])[0]
|
|
387
|
+
if exp_crc != got_crc:
|
|
388
|
+
raise CRCError(exp_crc, got_crc)
|
|
389
|
+
return raw_data[:length]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class Table:
|
|
393
|
+
"""Objects of this class can be used to hold the contents of a
|
|
394
|
+
CTid table in a Python-native format. Use method Table.encode()
|
|
395
|
+
to convert the table to a sequence of bytes, encoded as per CTid
|
|
396
|
+
specification or decode a sequence of bytes with method
|
|
397
|
+
Table.decode().
|
|
398
|
+
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
def __init__(self, data: bytes | None = None):
|
|
402
|
+
if data is not None:
|
|
403
|
+
data = check_table_data(data)
|
|
404
|
+
|
|
405
|
+
self.encoding = False
|
|
406
|
+
self.version = CTID_VERSION
|
|
407
|
+
self.mfg_id = 0
|
|
408
|
+
self.model = ""
|
|
409
|
+
self.serial_number: int = 0
|
|
410
|
+
self.sensor_type = SENSOR_TYPE_AC
|
|
411
|
+
self.r_source: float = 0
|
|
412
|
+
self.r_load: float = 0 # undefined
|
|
413
|
+
|
|
414
|
+
self.size: float = 0
|
|
415
|
+
self.rated_current: float = 0
|
|
416
|
+
self.voltage_at_rated_current: float = 0
|
|
417
|
+
self.phase_at_rated_current: float = 0
|
|
418
|
+
self.voltage_temp_coeff: float = 0
|
|
419
|
+
self.phase_temp_coeff: float = 0
|
|
420
|
+
self.cal_table = {1.5: [0, 0], 5: [0, 0], 15: [0, 0], 50: [0, 0]}
|
|
421
|
+
self.bias_voltage: float = 0
|
|
422
|
+
self.reserved = 0
|
|
423
|
+
self.mfg_info = 0
|
|
424
|
+
self.scale: float = 0
|
|
425
|
+
self.offset: float = 0
|
|
426
|
+
self.delay: float = 0
|
|
427
|
+
self.sensor_unit: int = 0
|
|
428
|
+
self.threshold: float = 0
|
|
429
|
+
self.hysteresis: float = 0
|
|
430
|
+
self.debounce_time: int = 0
|
|
431
|
+
self.edge_mask: int = 0
|
|
432
|
+
self.ntc_a: float = 0
|
|
433
|
+
self.ntc_b: float = 0
|
|
434
|
+
self.ntc_c: float = 0
|
|
435
|
+
self.ntc_m: float = 0
|
|
436
|
+
self.ntc_n: float = 0
|
|
437
|
+
self.ntc_k: float = 0
|
|
438
|
+
|
|
439
|
+
# internal data:
|
|
440
|
+
self._raw_data = b""
|
|
441
|
+
self._raw_offset = 0
|
|
442
|
+
|
|
443
|
+
if data is not None:
|
|
444
|
+
self.decode(data)
|
|
445
|
+
|
|
446
|
+
def __str__(self) -> str:
|
|
447
|
+
mfg_id_str = get_mfg_id(self.mfg_id)
|
|
448
|
+
if mfg_id_str is not None:
|
|
449
|
+
mfg_id_str = '"' + mfg_id_str + '"'
|
|
450
|
+
else:
|
|
451
|
+
mfg_id_str = f"{self.mfg_id:#04x}"
|
|
452
|
+
|
|
453
|
+
sensor_type_str = get_sensor_type_name(self.sensor_type)
|
|
454
|
+
if sensor_type_str is not None:
|
|
455
|
+
sensor_type_str = '"' + sensor_type_str + '"'
|
|
456
|
+
else:
|
|
457
|
+
sensor_type_str = f"{self.sensor_type:#1x}"
|
|
458
|
+
|
|
459
|
+
ret = (
|
|
460
|
+
f"CTid version={self.version:d}, sensor_type={sensor_type_str}, "
|
|
461
|
+
f'mfg_id={mfg_id_str}, model="{self.model}", '
|
|
462
|
+
)
|
|
463
|
+
if (
|
|
464
|
+
self.sensor_type >= SENSOR_TYPE_AC
|
|
465
|
+
and self.sensor_type <= SENSOR_TYPE_RC
|
|
466
|
+
):
|
|
467
|
+
rows = []
|
|
468
|
+
for lvl, val in self.cal_table.items():
|
|
469
|
+
rows.append(f"{repr(lvl)}%: {val[0]:+.2f}%/{val[1]:+.2f}°")
|
|
470
|
+
ret += (
|
|
471
|
+
f"size={self.size:.1f}mm, "
|
|
472
|
+
f"serial={self.serial_number}, "
|
|
473
|
+
f"current={self.rated_current:.1f}A, "
|
|
474
|
+
f"voltage={self.voltage_at_rated_current:.6f}V, "
|
|
475
|
+
f"bias={1e3 * self.bias_voltage:.3f}mV, "
|
|
476
|
+
f"phase={self.phase_at_rated_current:.2f}°, "
|
|
477
|
+
f"voltage_temp_coeff={self.voltage_temp_coeff:.0f}ppm/°C, "
|
|
478
|
+
f"phase_temp_coeff={self.phase_temp_coeff:.1f}m°/°C, "
|
|
479
|
+
f"reserved={self.reserved:#02x}, "
|
|
480
|
+
f"mfg_info={self.mfg_info:#02x}, "
|
|
481
|
+
f"Rs={self.r_source:g}, "
|
|
482
|
+
f"Rl={self.r_load:g}, "
|
|
483
|
+
"cal={" + ", ".join(rows) + "}"
|
|
484
|
+
)
|
|
485
|
+
elif self.sensor_type == SENSOR_TYPE_LINEAR:
|
|
486
|
+
unit = get_sensor_unit(self.sensor_unit)
|
|
487
|
+
ret += (
|
|
488
|
+
f"scale={self.scale:g}{unit}/V "
|
|
489
|
+
f"offset={self.offset:g}{unit} "
|
|
490
|
+
f"delay={self.delay:g}μs"
|
|
491
|
+
)
|
|
492
|
+
elif self.sensor_type == SENSOR_TYPE_TEMP_LINEAR:
|
|
493
|
+
ret += f"scale={self.scale:g}°C/V offset={self.offset:g}°C"
|
|
494
|
+
elif self.sensor_type == SENSOR_TYPE_TEMP_NTC:
|
|
495
|
+
ret += (
|
|
496
|
+
f"A={self.ntc_a:g} "
|
|
497
|
+
f"B={self.ntc_b:g} "
|
|
498
|
+
f"C={self.ntc_c:g} "
|
|
499
|
+
f"M={self.ntc_m:g} "
|
|
500
|
+
f"N={self.ntc_n:g} "
|
|
501
|
+
f"K={self.ntc_k:g}"
|
|
502
|
+
)
|
|
503
|
+
elif self.sensor_type == SENSOR_TYPE_PULSE:
|
|
504
|
+
ret += (
|
|
505
|
+
f"threshold={self.threshold:g}±{self.hysteresis:g}V "
|
|
506
|
+
f"debounce={self.debounce_time:d}ms "
|
|
507
|
+
f"edge={self.edge_mask:#x}"
|
|
508
|
+
)
|
|
509
|
+
else:
|
|
510
|
+
pass
|
|
511
|
+
ret += "}"
|
|
512
|
+
return ret
|
|
513
|
+
|
|
514
|
+
def m_u2(self, name: str, scale: float = 1, unit: str | None = None):
|
|
515
|
+
fmt = ">B"
|
|
516
|
+
if self.encoding:
|
|
517
|
+
val = fix(getattr(self, name), 2, False, name, unit, scale)
|
|
518
|
+
self._raw_data += struct.pack(fmt, val << 6)
|
|
519
|
+
else:
|
|
520
|
+
val = struct.unpack_from(fmt, self._raw_data, self._raw_offset)[0]
|
|
521
|
+
self._raw_offset += struct.calcsize(fmt)
|
|
522
|
+
setattr(self, name, unfix(val >> 6, scale))
|
|
523
|
+
|
|
524
|
+
def m_u8(self, name: str, scale: float = 1, unit: str | None = None):
|
|
525
|
+
fmt = ">B"
|
|
526
|
+
if self.encoding:
|
|
527
|
+
val = fix(getattr(self, name), 8, False, name, unit, scale)
|
|
528
|
+
self._raw_data += struct.pack(fmt, val)
|
|
529
|
+
else:
|
|
530
|
+
val = struct.unpack_from(fmt, self._raw_data, self._raw_offset)[0]
|
|
531
|
+
self._raw_offset += struct.calcsize(fmt)
|
|
532
|
+
setattr(self, name, unfix(val, scale))
|
|
533
|
+
|
|
534
|
+
def m_s8(
|
|
535
|
+
self,
|
|
536
|
+
name: str,
|
|
537
|
+
scale: float = 1,
|
|
538
|
+
unit: str | None = None,
|
|
539
|
+
idx1: float | None = None,
|
|
540
|
+
idx2: int | None = None,
|
|
541
|
+
):
|
|
542
|
+
fmt = ">b"
|
|
543
|
+
if self.encoding:
|
|
544
|
+
if idx1 is not None and idx2 is not None:
|
|
545
|
+
val = fix(
|
|
546
|
+
getattr(self, name)[idx1][idx2], 8, True, name, unit, scale
|
|
547
|
+
)
|
|
548
|
+
else:
|
|
549
|
+
val = fix(getattr(self, name), 8, True, name, unit, scale)
|
|
550
|
+
self._raw_data += struct.pack(fmt, val)
|
|
551
|
+
else:
|
|
552
|
+
val = struct.unpack_from(fmt, self._raw_data, self._raw_offset)[0]
|
|
553
|
+
self._raw_offset += struct.calcsize(fmt)
|
|
554
|
+
if idx1 is not None and idx2 is not None:
|
|
555
|
+
t = getattr(self, name)
|
|
556
|
+
t[idx1][idx2] = unfix(val, scale)
|
|
557
|
+
else:
|
|
558
|
+
setattr(self, name, unfix(val, scale))
|
|
559
|
+
|
|
560
|
+
def m_4s10(self, name: str, levels: list[float]):
|
|
561
|
+
"""Pack/unpack four signed 10-bit words into/from 5 unsigned bytes.
|
|
562
|
+
|
|
563
|
+
Required arguments:
|
|
564
|
+
|
|
565
|
+
name -- The name of the CTid parameter being packed/unpacked
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
levels -- The signal levels (in percent) by which the table is
|
|
569
|
+
indexed.
|
|
570
|
+
|
|
571
|
+
"""
|
|
572
|
+
fmt = 5 * "B"
|
|
573
|
+
t = getattr(self, name)
|
|
574
|
+
if self.encoding:
|
|
575
|
+
v0 = fix(t[levels[0]][0], 10, True, name, "%", 0.01)
|
|
576
|
+
v1 = fix(t[levels[0]][1], 10, True, name, "°", 0.01)
|
|
577
|
+
v2 = fix(t[levels[1]][0], 10, True, name, "%", 0.01)
|
|
578
|
+
v3 = fix(t[levels[1]][1], 10, True, name, "°", 0.01)
|
|
579
|
+
bs = (
|
|
580
|
+
(v0 >> 2) & 0xFF,
|
|
581
|
+
((v0 & 0x03) << 6) | ((v1 >> 4) & 0x3F),
|
|
582
|
+
((v1 & 0x0F) << 4) | ((v2 >> 6) & 0x0F),
|
|
583
|
+
((v2 & 0x3F) << 2) | ((v3 >> 8) & 0x03),
|
|
584
|
+
(v3 & 0xFF),
|
|
585
|
+
)
|
|
586
|
+
self._raw_data += struct.pack(fmt, *bs)
|
|
587
|
+
else:
|
|
588
|
+
bs = struct.unpack_from(fmt, self._raw_data, self._raw_offset)
|
|
589
|
+
self._raw_offset += struct.calcsize(fmt)
|
|
590
|
+
v0 = s10((bs[0] << 2) | ((bs[1] >> 6) & 0x03))
|
|
591
|
+
v1 = s10(((bs[1] & 0x3F) << 4) | ((bs[2] >> 4) & 0x0F))
|
|
592
|
+
v2 = s10(((bs[2] & 0x0F) << 6) | ((bs[3] >> 2) & 0x3F))
|
|
593
|
+
v3 = s10(((bs[3] & 0x03) << 8) | bs[4])
|
|
594
|
+
t[levels[0]][0] = unfix(v0, 0.01)
|
|
595
|
+
t[levels[0]][1] = unfix(v1, 0.01)
|
|
596
|
+
t[levels[1]][0] = unfix(v2, 0.01)
|
|
597
|
+
t[levels[1]][1] = unfix(v3, 0.01)
|
|
598
|
+
|
|
599
|
+
def m_u16(self, name: str, scale: float = 1, unit: str | None = None):
|
|
600
|
+
fmt = ">H"
|
|
601
|
+
if self.encoding:
|
|
602
|
+
val = fix(getattr(self, name), 16, False, name, unit, scale)
|
|
603
|
+
self._raw_data += struct.pack(fmt, val)
|
|
604
|
+
else:
|
|
605
|
+
val = struct.unpack_from(fmt, self._raw_data, self._raw_offset)[0]
|
|
606
|
+
self._raw_offset += struct.calcsize(fmt)
|
|
607
|
+
setattr(self, name, unfix(val, scale))
|
|
608
|
+
|
|
609
|
+
def m_s16(self, name: str, scale: float = 1, unit: str | None = None):
|
|
610
|
+
fmt = ">h"
|
|
611
|
+
if self.encoding:
|
|
612
|
+
val = fix(getattr(self, name), 16, True, name, unit, scale)
|
|
613
|
+
self._raw_data += struct.pack(fmt, val)
|
|
614
|
+
else:
|
|
615
|
+
val = struct.unpack_from(fmt, self._raw_data, self._raw_offset)[0]
|
|
616
|
+
self._raw_offset += struct.calcsize(fmt)
|
|
617
|
+
setattr(self, name, unfix(val, scale))
|
|
618
|
+
|
|
619
|
+
def m_d16(self, name: str, unit: str):
|
|
620
|
+
fmt = ">H"
|
|
621
|
+
if self.encoding:
|
|
622
|
+
val = getattr(self, name)
|
|
623
|
+
self._raw_data += struct.pack(fmt, decimal16(val, name, unit))
|
|
624
|
+
else:
|
|
625
|
+
val = struct.unpack_from(fmt, self._raw_data, self._raw_offset)[0]
|
|
626
|
+
self._raw_offset += struct.calcsize(fmt)
|
|
627
|
+
setattr(self, name, undecimal16(val))
|
|
628
|
+
|
|
629
|
+
def m_s12(self, name: str, scale: float = 1, unit: str | None = None):
|
|
630
|
+
fmt = ">H"
|
|
631
|
+
if self.encoding:
|
|
632
|
+
s12 = fix(getattr(self, name), 12, True, name, unit, scale)
|
|
633
|
+
if s12 < 0:
|
|
634
|
+
s12 &= 0xFFF
|
|
635
|
+
self._raw_data += struct.pack(fmt, s12 << 4)
|
|
636
|
+
else:
|
|
637
|
+
u16 = struct.unpack_from(fmt, self._raw_data, self._raw_offset)[0]
|
|
638
|
+
self._raw_offset += struct.calcsize(fmt)
|
|
639
|
+
s12 = (u16 >> 4) & 0x0FFF
|
|
640
|
+
if s12 >= 0x800:
|
|
641
|
+
s12 -= 0x1000
|
|
642
|
+
setattr(self, name, unfix(s12, scale))
|
|
643
|
+
|
|
644
|
+
def m_u4_s12(
|
|
645
|
+
self,
|
|
646
|
+
name4: str,
|
|
647
|
+
scale4: float,
|
|
648
|
+
unit4: str | None,
|
|
649
|
+
name12: str,
|
|
650
|
+
scale12: float = 1,
|
|
651
|
+
unit12: str | None = None,
|
|
652
|
+
):
|
|
653
|
+
fmt = ">H"
|
|
654
|
+
if self.encoding:
|
|
655
|
+
u4 = fix(getattr(self, name4), 4, False, name4, unit4, scale4)
|
|
656
|
+
s12 = fix(getattr(self, name12), 12, True, name12, unit12, scale12)
|
|
657
|
+
if s12 < 0:
|
|
658
|
+
s12 &= 0xFFF
|
|
659
|
+
self._raw_data += struct.pack(fmt, (u4 << 12) | s12)
|
|
660
|
+
else:
|
|
661
|
+
u16 = struct.unpack_from(fmt, self._raw_data, self._raw_offset)[0]
|
|
662
|
+
self._raw_offset += struct.calcsize(fmt)
|
|
663
|
+
setattr(self, name4, unfix((u16 >> 12) & 0xF, scale4))
|
|
664
|
+
s12 = u16 & 0x0FFF
|
|
665
|
+
if s12 >= 0x800:
|
|
666
|
+
s12 -= 0x1000
|
|
667
|
+
setattr(self, name12, unfix(s12, scale12))
|
|
668
|
+
|
|
669
|
+
def m_f12_f12(self, name1: str, unit1: str, name2: str, unit2: str):
|
|
670
|
+
fmt = ">BH"
|
|
671
|
+
if self.encoding:
|
|
672
|
+
f1 = float12(getattr(self, name1), name1, unit1)
|
|
673
|
+
f2 = float12(getattr(self, name2), name2, unit2)
|
|
674
|
+
u24 = (f1 << 12) | f2
|
|
675
|
+
self._raw_data += struct.pack(fmt, u24 >> 16, u24 & 0xFFFF)
|
|
676
|
+
else:
|
|
677
|
+
val = struct.unpack_from(fmt, self._raw_data, self._raw_offset)
|
|
678
|
+
u24 = (val[0] << 16) | val[1]
|
|
679
|
+
self._raw_offset += struct.calcsize(fmt)
|
|
680
|
+
setattr(self, name1, round(unfloat12((u24 >> 12) & 0xFFF)))
|
|
681
|
+
setattr(self, name2, round(unfloat12((u24 >> 0) & 0xFFF)))
|
|
682
|
+
|
|
683
|
+
def m_u24(self, name: str, scale: float = 1, unit: str | None = None):
|
|
684
|
+
fmt = ">BH"
|
|
685
|
+
if self.encoding:
|
|
686
|
+
val = fix(getattr(self, name), 24, False, name, unit, scale)
|
|
687
|
+
self._raw_data += struct.pack(
|
|
688
|
+
fmt, (val >> 16) & 0xFF, val & 0xFFFF
|
|
689
|
+
)
|
|
690
|
+
else:
|
|
691
|
+
val = struct.unpack_from(fmt, self._raw_data, self._raw_offset)
|
|
692
|
+
val = (val[0] << 16) | val[1]
|
|
693
|
+
self._raw_offset += struct.calcsize(fmt)
|
|
694
|
+
setattr(self, name, unfix(val, scale))
|
|
695
|
+
|
|
696
|
+
def m_f32(self, name: str):
|
|
697
|
+
fmt = ">f"
|
|
698
|
+
if self.encoding:
|
|
699
|
+
self._raw_data += struct.pack(fmt, getattr(self, name))
|
|
700
|
+
else:
|
|
701
|
+
val = struct.unpack_from(fmt, self._raw_data, self._raw_offset)
|
|
702
|
+
self._raw_offset += struct.calcsize(fmt)
|
|
703
|
+
setattr(self, name, val[0])
|
|
704
|
+
|
|
705
|
+
def m_utf8_4(self, name: str):
|
|
706
|
+
if self.encoding:
|
|
707
|
+
val = getattr(self, name).encode("utf-8")
|
|
708
|
+
if len(val) > 4:
|
|
709
|
+
raise Error(
|
|
710
|
+
f"{name} `{getattr(self, name)}' is {len(val)} bytes "
|
|
711
|
+
"long in UTF-8 but only up to 4 bytes are allowed."
|
|
712
|
+
)
|
|
713
|
+
while len(val) < 4:
|
|
714
|
+
val += b"\0"
|
|
715
|
+
self._raw_data += val
|
|
716
|
+
else:
|
|
717
|
+
raw = b""
|
|
718
|
+
for i in range(self._raw_offset, self._raw_offset + 4):
|
|
719
|
+
if self._raw_data[i] == 0:
|
|
720
|
+
break
|
|
721
|
+
raw += bytearray((self._raw_data[i],))
|
|
722
|
+
self._raw_offset += 4
|
|
723
|
+
setattr(self, name, raw.decode())
|
|
724
|
+
|
|
725
|
+
def m_utf8_8(self, name: str):
|
|
726
|
+
if self.encoding:
|
|
727
|
+
val = getattr(self, name).encode("utf-8")
|
|
728
|
+
if len(val) > 8:
|
|
729
|
+
raise Error(
|
|
730
|
+
f"{name} `{getattr(self, name)}' is {len(val)} bytes "
|
|
731
|
+
"long in UTF-8 but only up to 8 bytes are allowed."
|
|
732
|
+
)
|
|
733
|
+
while len(val) < 8:
|
|
734
|
+
val += b"\0"
|
|
735
|
+
self._raw_data += val
|
|
736
|
+
else:
|
|
737
|
+
raw = b""
|
|
738
|
+
for i in range(self._raw_offset, self._raw_offset + 8):
|
|
739
|
+
if self._raw_data[i] == 0:
|
|
740
|
+
break
|
|
741
|
+
raw += bytearray((self._raw_data[i],))
|
|
742
|
+
self._raw_offset += 8
|
|
743
|
+
setattr(self, name, raw.decode())
|
|
744
|
+
|
|
745
|
+
def marshall_params_ct(self):
|
|
746
|
+
"""Current Transducer Parameters"""
|
|
747
|
+
self.m_u16("rated_current", 0.1, "A")
|
|
748
|
+
self.m_u16("voltage_at_rated_current", 10e-6, "V")
|
|
749
|
+
self.m_u16("size", 0.1, "mm")
|
|
750
|
+
self.m_s12("phase_at_rated_current", 0.01, "°")
|
|
751
|
+
self.m_s8("voltage_temp_coeff", 5, "ppm/°C")
|
|
752
|
+
self.m_s8("phase_temp_coeff", 0.5, "m°/°C")
|
|
753
|
+
if self.version >= 5:
|
|
754
|
+
keys = list(self.cal_table.keys())
|
|
755
|
+
if len(keys) != 4:
|
|
756
|
+
raise Error(f"Cal table has {len(keys)} rows; expected 4.")
|
|
757
|
+
self.m_4s10("cal_table", keys[0:2])
|
|
758
|
+
self.m_4s10("cal_table", keys[2:4])
|
|
759
|
+
else:
|
|
760
|
+
for k in sorted(self.cal_table.keys()):
|
|
761
|
+
self.m_s8("cal_table", 0.02, "%", idx1=k, idx2=0)
|
|
762
|
+
self.m_s8("cal_table", 0.02, "\u00b0", idx1=k, idx2=1)
|
|
763
|
+
self.m_s16("bias_voltage", 1e-6, "V")
|
|
764
|
+
|
|
765
|
+
def marshall_params_basic_linear(self):
|
|
766
|
+
"""Basic linear Parameters (for linear temp. and generic linear
|
|
767
|
+
sensors)."""
|
|
768
|
+
self.m_f32("scale")
|
|
769
|
+
self.m_f32("offset")
|
|
770
|
+
|
|
771
|
+
def marshall_params_linear(self):
|
|
772
|
+
"""Linear Parameters."""
|
|
773
|
+
self.marshall_params_basic_linear()
|
|
774
|
+
self.m_s16("delay", 0.01, "μs")
|
|
775
|
+
self.m_u16("sensor_unit")
|
|
776
|
+
|
|
777
|
+
def marshall_params_ntc(self):
|
|
778
|
+
"""NTC Parameters."""
|
|
779
|
+
self.m_f32("ntc_a")
|
|
780
|
+
self.m_f32("ntc_b")
|
|
781
|
+
self.m_f32("ntc_c")
|
|
782
|
+
self.m_f32("ntc_m")
|
|
783
|
+
if self.version == 3:
|
|
784
|
+
self.m_d16("ntc_r1", "Ω")
|
|
785
|
+
else:
|
|
786
|
+
self.m_f32("ntc_n")
|
|
787
|
+
self.m_f32("ntc_k")
|
|
788
|
+
|
|
789
|
+
def marshall_params_pulse(self):
|
|
790
|
+
"""Pulse Parameters."""
|
|
791
|
+
self.m_u16("threshold", 10e-6, "V")
|
|
792
|
+
self.m_u16("hysteresis", 10e-6, "V")
|
|
793
|
+
self.m_u8("debounce_time")
|
|
794
|
+
self.m_u2("edge_mask")
|
|
795
|
+
|
|
796
|
+
def marshall_params(self):
|
|
797
|
+
if (
|
|
798
|
+
self.sensor_type >= SENSOR_TYPE_AC
|
|
799
|
+
and self.sensor_type <= SENSOR_TYPE_RC
|
|
800
|
+
):
|
|
801
|
+
self.marshall_params_ct()
|
|
802
|
+
elif self.sensor_type == SENSOR_TYPE_LINEAR:
|
|
803
|
+
self.marshall_params_linear()
|
|
804
|
+
elif self.sensor_type == SENSOR_TYPE_TEMP_LINEAR:
|
|
805
|
+
self.marshall_params_basic_linear()
|
|
806
|
+
elif self.sensor_type == SENSOR_TYPE_TEMP_NTC:
|
|
807
|
+
self.marshall_params_ntc()
|
|
808
|
+
elif self.sensor_type == SENSOR_TYPE_PULSE:
|
|
809
|
+
self.marshall_params_pulse()
|
|
810
|
+
|
|
811
|
+
max_size = 43 if self.version >= 4 else 33
|
|
812
|
+
|
|
813
|
+
if len(self._raw_data) < max_size:
|
|
814
|
+
self._raw_data += (max_size - len(self._raw_data)) * b"\0"
|
|
815
|
+
elif len(self._raw_data) > max_size:
|
|
816
|
+
raise Error("CTid table too big", len(self._raw_data), max_size)
|
|
817
|
+
|
|
818
|
+
def marshall(self):
|
|
819
|
+
self.m_u8("version")
|
|
820
|
+
self.m_u16("mfg_id")
|
|
821
|
+
if self.version >= 4:
|
|
822
|
+
# v4 or newer
|
|
823
|
+
self.m_utf8_8("model")
|
|
824
|
+
self.m_u8("reserved")
|
|
825
|
+
self.m_u24("serial_number")
|
|
826
|
+
self.m_u8("sensor_type")
|
|
827
|
+
self.m_f12_f12("r_source", "Ω", "r_load", "Ω")
|
|
828
|
+
self.marshall_params()
|
|
829
|
+
else:
|
|
830
|
+
self.m_utf8_4("model")
|
|
831
|
+
if self.version == 3:
|
|
832
|
+
# v3
|
|
833
|
+
self.m_u24("serial_number")
|
|
834
|
+
self.m_u8("sensor_type")
|
|
835
|
+
self.m_f12_f12("r_source", "Ω", "r_load", "Ω")
|
|
836
|
+
self.m_u8("reserved")
|
|
837
|
+
self.marshall_params()
|
|
838
|
+
else:
|
|
839
|
+
# v1 or v2
|
|
840
|
+
self.m_u16("size", 0.1, "mm")
|
|
841
|
+
self.m_u24("serial_number")
|
|
842
|
+
self.m_u16("rated_current", 0.1, "A")
|
|
843
|
+
self.m_u16("voltage_at_rated_current", 10e-6, "V")
|
|
844
|
+
self.m_u4_s12(
|
|
845
|
+
"sensor_type",
|
|
846
|
+
1,
|
|
847
|
+
None,
|
|
848
|
+
"phase_at_rated_current",
|
|
849
|
+
0.01,
|
|
850
|
+
"°",
|
|
851
|
+
)
|
|
852
|
+
self.m_s8("voltage_temp_coeff", 5, "ppm/°C")
|
|
853
|
+
self.m_s8("phase_temp_coeff", 0.5, "m°/°C")
|
|
854
|
+
for k in sorted(self.cal_table.keys()):
|
|
855
|
+
self.m_s8("cal_table", 0.02, "%", idx1=k, idx2=0)
|
|
856
|
+
self.m_s8("cal_table", 0.02, "°", idx1=k, idx2=1)
|
|
857
|
+
self.m_u8("reserved")
|
|
858
|
+
self.m_u8("mfg_info")
|
|
859
|
+
if self.version > 1:
|
|
860
|
+
self.m_f12_f12("r_source", "Ω", "r_load", "Ω")
|
|
861
|
+
|
|
862
|
+
def encode(self, version: int = CTID_VERSION):
|
|
863
|
+
"""Encode the table contents and store it as a sequence of
|
|
864
|
+
bytes in property `raw_data`.
|
|
865
|
+
|
|
866
|
+
Keyword arguments:
|
|
867
|
+
|
|
868
|
+
version -- The version of the CTid specification according to
|
|
869
|
+
which to produce the table (default latest version).
|
|
870
|
+
|
|
871
|
+
"""
|
|
872
|
+
self.encoding = True
|
|
873
|
+
self._raw_data = b""
|
|
874
|
+
self._raw_offset = 0
|
|
875
|
+
self.version = version
|
|
876
|
+
if version < 2 and (self.r_source != 0.0 or self.r_load != 0.0):
|
|
877
|
+
raise Error(
|
|
878
|
+
"Unable to encode r_source and r_load in "
|
|
879
|
+
f"CTid table version {version}."
|
|
880
|
+
)
|
|
881
|
+
try:
|
|
882
|
+
self.marshall()
|
|
883
|
+
except KeyError as e:
|
|
884
|
+
raise Error("Required parameter missing from CTid table.") from e
|
|
885
|
+
except struct.error as e:
|
|
886
|
+
raise Error("CTid table parameter has an invalid value.") from e
|
|
887
|
+
if self.version == 1:
|
|
888
|
+
# v1 used an 8-bit CRC
|
|
889
|
+
self._raw_data += struct.pack(">B", crc8_rohc(self._raw_data))
|
|
890
|
+
assert len(self._raw_data) == 31 # excluding start symbol
|
|
891
|
+
else:
|
|
892
|
+
self._raw_data += struct.pack(">H", crc16_modbus(self._raw_data))
|
|
893
|
+
if self.version < 4:
|
|
894
|
+
# v2-3
|
|
895
|
+
assert len(self._raw_data) == 35 # excluding start symbol
|
|
896
|
+
else:
|
|
897
|
+
# v4 and on...
|
|
898
|
+
assert len(self._raw_data) == 45 # excluding start symbol
|
|
899
|
+
return self._raw_data
|
|
900
|
+
|
|
901
|
+
def decode(self, raw_data: bytes):
|
|
902
|
+
"""Load the table from a sequence of bytes.
|
|
903
|
+
|
|
904
|
+
Required arguments:
|
|
905
|
+
|
|
906
|
+
raw_data -- The byte sequence to decode. Note: the
|
|
907
|
+
start-symbol and the CRC must not be part of this data.
|
|
908
|
+
The CRC should already have been confirmed to be correct
|
|
909
|
+
before calling this method.
|
|
910
|
+
|
|
911
|
+
"""
|
|
912
|
+
self.encoding = False
|
|
913
|
+
self._raw_data = raw_data
|
|
914
|
+
self._raw_offset = 0
|
|
915
|
+
self.marshall()
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def bitstuff(data: bytes) -> bytes:
|
|
919
|
+
"""Get the bit-stuffed version of the data, prefixed with the
|
|
920
|
+
start symbol.
|
|
921
|
+
|
|
922
|
+
Required arguments:
|
|
923
|
+
|
|
924
|
+
data -- The data bytes to bit-stuff.
|
|
925
|
+
|
|
926
|
+
"""
|
|
927
|
+
bs = BitStuffer(bytes((START_SYM,)))
|
|
928
|
+
for b in data:
|
|
929
|
+
bs.append(b)
|
|
930
|
+
return bs.get_output()
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def unstuff(bitstream: bytes) -> bytearray:
|
|
934
|
+
"""Remove the start-symbol and the bit-stuffing from the
|
|
935
|
+
bitstream.
|
|
936
|
+
|
|
937
|
+
Required arguments:
|
|
938
|
+
|
|
939
|
+
bitstream -- The bitstream to unstuff.
|
|
940
|
+
|
|
941
|
+
"""
|
|
942
|
+
if bitstream[0] != START_SYM:
|
|
943
|
+
raise Error("Bitstream missing start symbol.", bitstream[0], START_SYM)
|
|
944
|
+
|
|
945
|
+
data = bytearray()
|
|
946
|
+
run_length = 0
|
|
947
|
+
byte = 0x00
|
|
948
|
+
num_bits = 0
|
|
949
|
+
for b in bitstream[1:]:
|
|
950
|
+
mask = 0x80
|
|
951
|
+
while mask != 0:
|
|
952
|
+
if run_length >= 7:
|
|
953
|
+
# drop a stuffer bit
|
|
954
|
+
run_length = 0
|
|
955
|
+
else:
|
|
956
|
+
if b & mask:
|
|
957
|
+
byte |= 1 << (7 - num_bits)
|
|
958
|
+
run_length += 1
|
|
959
|
+
else:
|
|
960
|
+
run_length = 0
|
|
961
|
+
num_bits += 1
|
|
962
|
+
if num_bits >= 8:
|
|
963
|
+
data.append(byte)
|
|
964
|
+
byte = 0
|
|
965
|
+
num_bits = 0
|
|
966
|
+
mask >>= 1
|
|
967
|
+
return data
|