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.
Files changed (44) hide show
  1. egauge/ctid/__init__.py +7 -0
  2. egauge/ctid/bit_stuffer.py +65 -0
  3. egauge/ctid/ctid.py +967 -0
  4. egauge/ctid/encoder.py +436 -0
  5. egauge/ctid/intel_hex_encoder.py +98 -0
  6. egauge/ctid/waveform.py +299 -0
  7. egauge/examples/data/test-ctid-decoder.raw +0 -0
  8. egauge/examples/test_capture.py +77 -0
  9. egauge/examples/test_common.py +26 -0
  10. egauge/examples/test_ctid.py +89 -0
  11. egauge/examples/test_ctid_decoder.py +93 -0
  12. egauge/examples/test_local.py +201 -0
  13. egauge/examples/test_register.py +104 -0
  14. egauge/loggers.py +72 -0
  15. egauge/pyside/__init__.py +0 -0
  16. egauge/pyside/ansi2html.py +112 -0
  17. egauge/pyside/terminal.py +295 -0
  18. egauge/webapi/__init__.py +34 -0
  19. egauge/webapi/auth.py +364 -0
  20. egauge/webapi/cloud/__init__.py +30 -0
  21. egauge/webapi/cloud/credentials.py +86 -0
  22. egauge/webapi/cloud/credentials_dialog.py +58 -0
  23. egauge/webapi/cloud/gui/credentials_dialog.py +100 -0
  24. egauge/webapi/cloud/serial_number.py +276 -0
  25. egauge/webapi/device/__init__.py +38 -0
  26. egauge/webapi/device/capture.py +453 -0
  27. egauge/webapi/device/ctid_info.py +553 -0
  28. egauge/webapi/device/device.py +349 -0
  29. egauge/webapi/device/local.py +268 -0
  30. egauge/webapi/device/physical_quantity.py +439 -0
  31. egauge/webapi/device/physical_units.py +473 -0
  32. egauge/webapi/device/register.py +338 -0
  33. egauge/webapi/device/register_row.py +145 -0
  34. egauge/webapi/device/register_type.py +851 -0
  35. egauge/webapi/device/slop.py +334 -0
  36. egauge/webapi/device/virtual_register.py +353 -0
  37. egauge/webapi/error.py +34 -0
  38. egauge/webapi/json_api.py +332 -0
  39. egauge_python-0.9.8.dist-info/METADATA +148 -0
  40. egauge_python-0.9.8.dist-info/RECORD +44 -0
  41. egauge_python-0.9.8.dist-info/WHEEL +5 -0
  42. egauge_python-0.9.8.dist-info/entry_points.txt +2 -0
  43. egauge_python-0.9.8.dist-info/licenses/LICENSE +22 -0
  44. egauge_python-0.9.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,473 @@
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
+ from dataclasses import dataclass
31
+ from typing import Callable
32
+
33
+
34
+ class Error(Exception):
35
+ """Base class for any error raised by this module."""
36
+
37
+
38
+ @dataclass
39
+ class PhysicalValue:
40
+ """A physical value has a unit attached to its numeric value."""
41
+
42
+ value: float
43
+ unit: str
44
+
45
+ def __str__(self):
46
+ return f"{self.value}{self.unit}"
47
+
48
+
49
+ @dataclass
50
+ class PhysicalUnitConversion:
51
+ """An object of this class handles conversion from one unit to another
52
+ and back."""
53
+
54
+ #
55
+ # True if the conversion requires specifying the time parameters
56
+ # to `calc` and `inverse`. This is necessary, for example to
57
+ # convert from C*s (celsius-seconds) to F*s (Fahrenheit-seconds).
58
+ #
59
+ time_dependent: bool
60
+ #
61
+ # Given a primary unit value, calculate the equivalent value in
62
+ # this unit.
63
+ #
64
+ # First argument is the primary-unit value to convert. Second
65
+ # argument is is the number of seconds over which the value was
66
+ # measured. This argument is only needed if `time_dependent` is
67
+ # True.
68
+ calc: Callable[[float, float | None], float]
69
+ #
70
+ # Given a value in this unit, convert it back to a primary-unit value.
71
+ #
72
+ # First argument is the value to convert. Second argument is the
73
+ # number of seconds over which the value was measured. This
74
+ # argument is only needed if `time_dependent` is True.
75
+ inverse: Callable[[float, float | None], float]
76
+ #
77
+ # Given a primary-unit change (difference) in value, calculate the
78
+ # equivalent difference in this unit. This is defined only if the
79
+ # formula is not the same as `calc`.
80
+ #
81
+ # The first argument is the measured difference to convert. The
82
+ # second argument is the number of seconds over which the
83
+ # difference was measured. This argument is only needed if
84
+ # `time_dependent` is True.
85
+ diff_calc: Callable[[float, float | None], float] | None = None
86
+ #
87
+ # Given a difference in this unit, convert it back to a difference
88
+ # in the primary unit. This is defined only if the formula is not
89
+ # the same as `inverse`.
90
+ #
91
+ # First argument is the difference to convert. The second
92
+ # argument is the number of seconds over which the difference was
93
+ # measured. This argument is only needed if `time_dependent` is
94
+ # True.
95
+ diff_inverse: Callable[[float, float | None], float] | None = None
96
+
97
+
98
+ class PhysicalUnit:
99
+ """Each physical unit object has a unit name and may have one or more
100
+ scaled version of this unit."""
101
+
102
+ def __init__(self, name: str, description: str | None):
103
+ self.name = name
104
+ self.description = description
105
+ # Dictionary of scaled units or None if there aren't any. The
106
+ # index is the name of the scaled unit, the value is the scale
107
+ # factor to use when converting to that scaled unit.
108
+ self.scaled: dict[str, float] | None = None
109
+ # Documentation for a particular scaled unit or None if there
110
+ # aren't any such documentation strings.
111
+ self.scaled_description: dict[str, str] | None = None
112
+
113
+ def add_scaled(
114
+ self, name: str, factor: float, description: str | None = None
115
+ ):
116
+ """Add a scaled version of the unit (e.g., for 'A' (ampere), this
117
+ might be 'mA' (milli ampere) or 'kA' (kilo ampere).
118
+
119
+ Required arguments:
120
+
121
+ name -- The name of the scaled unit.
122
+
123
+ factor -- The number by which to multiply a value by to get
124
+ the equivalent value in the scaled unit.
125
+
126
+ Keyword arguments:
127
+
128
+ description -- Optional description of the scaled unit
129
+ (default None).
130
+
131
+ """
132
+ if not self.scaled:
133
+ self.scaled = {}
134
+ if name in self.scaled:
135
+ raise Error("Scaled unit name already exists.", name)
136
+ self.scaled[name] = factor
137
+ if description is not None:
138
+ if not self.scaled_description:
139
+ self.scaled_description = {}
140
+ self.scaled_description[name] = description
141
+
142
+
143
+ class PrimaryUnit(PhysicalUnit):
144
+ """Each primary unit object is a physical unit that may have zero or
145
+ more alternate units."""
146
+
147
+ def __init__(self, name: str, description: str | None = None):
148
+ super().__init__(name, description)
149
+ # Dictionary of alternate units available for this primary
150
+ # unit. The index is the name of the alternate unit, the
151
+ # value is an alternate unit object.
152
+ self.alternates: dict[str, "AlternateUnit"] | None = None
153
+
154
+ def add_alternate(self, alternate: "AlternateUnit"):
155
+ """Add an alternate unit to this primary unit."""
156
+ if not self.alternates:
157
+ self.alternates = {}
158
+ if alternate.name in self.alternates:
159
+ raise Error("Alternate unit name already exists.", alternate.name)
160
+ self.alternates[alternate.name] = alternate
161
+
162
+
163
+ class AlternateUnit(PhysicalUnit):
164
+ """Each alternate unit is a physical unit which has a parent that this
165
+ the primary unit and conversion functions to convert values
166
+ between the primary unit and the alternate unit."""
167
+
168
+ def __init__(
169
+ self,
170
+ name: str,
171
+ primary: PrimaryUnit,
172
+ conversion: PhysicalUnitConversion,
173
+ description: str | None = None,
174
+ ):
175
+ """Create an alternate unit object.
176
+
177
+ Required arguments:
178
+
179
+ name -- The name of the alternate unit.
180
+
181
+ primary -- The primary unit of the alternate unit.
182
+
183
+ conversion --- Physical unit conversion object describing the
184
+ conversion between the primary and alternate units.
185
+
186
+ Keyword arguments:
187
+
188
+ description -- Optional description of the alternate unit
189
+ (default None).
190
+
191
+ """
192
+ super().__init__(name, description)
193
+ self.primary = primary
194
+ self.conversion = conversion
195
+ primary.add_alternate(self)
196
+
197
+
198
+ class PhysicalUnits:
199
+ """An object of this class defines a set of physical units and methods
200
+ to convert between them."""
201
+
202
+ def __init__(self):
203
+ # Map from any recognized unit to its PhysicalUnit. For
204
+ # scaled units, this maps to the primary or alternate unit
205
+ # that defines the scaled unit.
206
+ self._map: dict[str, PhysicalUnit] = {}
207
+
208
+ def add(self, unit: PhysicalUnit):
209
+ """Add a physical unit.
210
+
211
+ Raises Error if the unit's name is already known.
212
+
213
+ Required argument:
214
+
215
+ unit -- The physical unit to add.
216
+
217
+ """
218
+ if unit.name in self._map:
219
+ raise Error("Unit name already exists.", unit.name)
220
+ self._map[unit.name] = unit
221
+
222
+ def add_scaled(
223
+ self,
224
+ unit: PhysicalUnit,
225
+ name: str,
226
+ factor: float,
227
+ description: str | None = None,
228
+ ):
229
+ """Add a scaled version of a unit.
230
+
231
+ Raises an Error if the scaled unit's name is already known.
232
+
233
+ Required arguments:
234
+
235
+ name -- The name of the scaled unit.
236
+
237
+ factor -- The number by which to multiply a value by to get
238
+ the equivalent value in the scaled unit.
239
+
240
+ Keyword arguments:
241
+
242
+ description -- Optional description of the scaled unit
243
+ (default None).
244
+
245
+ """
246
+ unit.add_scaled(name, factor, description)
247
+ if name in self._map:
248
+ raise Error("Scaled unit already exists.", name)
249
+ self._map[name] = unit
250
+
251
+ def convert(
252
+ self,
253
+ x: float,
254
+ from_unit: str,
255
+ to_unit: str,
256
+ is_diff: bool = False,
257
+ t: float | None = None,
258
+ ) -> float | None:
259
+ """Convert a value from a source unit to a destination unit.
260
+
261
+ Returns the value in the destination unit or `None` if the
262
+ source or destination unit is unknown or if the conversion
263
+ from the source unit to the destination unit is not possible.
264
+
265
+ Required arguments:
266
+
267
+ x -- The value to convert.
268
+
269
+ from_unit -- The unit of `x`
270
+
271
+ to_unit -- The destination unit to convert `x` to.
272
+
273
+ Keyword arguments:
274
+
275
+ is_diff -- Indicates that the value `x` is a difference (i.e.,
276
+ a relative value) rather than an absolute value (default
277
+ `False`).
278
+
279
+ t -- Indicates whether the conversion is time-dependent
280
+ (default None). If None, the conversion is independent of
281
+ time. If a number, the conversion is time-dependent and
282
+ `t` must the duration over which the value was measured.
283
+
284
+ """
285
+ if from_unit == to_unit:
286
+ return x
287
+
288
+ pu = self._map.get(from_unit)
289
+ if not isinstance(pu, PrimaryUnit):
290
+ return None
291
+
292
+ if pu.scaled and from_unit in pu.scaled:
293
+ # convert the value to the unscaled unit:
294
+ x /= pu.scaled[from_unit]
295
+ from_unit = pu.name
296
+ if from_unit == to_unit:
297
+ return x
298
+
299
+ if isinstance(pu, AlternateUnit):
300
+ # convert from alternate unit to primary unit:
301
+ c = pu.conversion
302
+ if is_diff and c.diff_inverse:
303
+ x = c.diff_inverse(x, t)
304
+ else:
305
+ x = c.inverse(x, t)
306
+ pu = pu.primary
307
+
308
+ primary = pu
309
+ from_unit = primary.name
310
+ if from_unit == to_unit:
311
+ return x
312
+
313
+ # convert from primary unit to target unit:
314
+ pu = self._map.get(to_unit)
315
+ if not pu:
316
+ return None
317
+
318
+ # verify that we can convert between the two units:
319
+ if isinstance(pu, AlternateUnit):
320
+ if pu.primary != primary:
321
+ return None
322
+ elif pu != primary:
323
+ return None
324
+
325
+ valid_unit = False
326
+ if primary.alternates:
327
+ au = primary.alternates.get(pu.name)
328
+ if au:
329
+ # convert to alternate unit:
330
+ pu = au
331
+ c = au.conversion
332
+ if is_diff and c.diff_calc:
333
+ x = c.diff_calc(x, t)
334
+ else:
335
+ x = c.calc(x, t)
336
+ valid_unit = True
337
+
338
+ if pu.scaled and to_unit in pu.scaled:
339
+ # apply scale factor:
340
+ x *= pu.scaled[to_unit]
341
+ valid_unit = True
342
+ return x if valid_unit else None
343
+
344
+ def auto_scale(self, pv: PhysicalValue) -> PhysicalValue:
345
+ """Automatically scale a physical value such that its absolute
346
+ value is either zero or in the range from 1..1000.
347
+
348
+ The method returns the scaled value or the original value if
349
+ scaling is not possible for any reason.
350
+
351
+ Required arguments:
352
+
353
+ pv -- The physical value to auto-scale.
354
+
355
+ """
356
+ pu = self._map.get(pv.unit)
357
+ if not pu or _well_scaled(pv.value):
358
+ return pv
359
+
360
+ if pv.unit != pu.name:
361
+ # convert to unscaled unit:
362
+ val = self.convert(pv.value, pv.unit, pu.name)
363
+
364
+ if not isinstance(val, float):
365
+ return pv # conversion is not possible
366
+
367
+ pv.value = val
368
+ pv.unit = pu.name
369
+ if _well_scaled(pv.value):
370
+ return pv
371
+
372
+ best = pv
373
+ if pu.scaled:
374
+ for scaled_unit, factor in pu.scaled.items():
375
+ value = pv.value * factor
376
+ if abs(value) >= 1.0:
377
+ best = PhysicalValue(value, scaled_unit)
378
+ if _well_scaled(value):
379
+ break
380
+ return best
381
+
382
+ def primary_unit(self, unit: str) -> str | None:
383
+ """Return the name of the primary unit of a unit name.
384
+
385
+ This method returns the primary unit name or `None` if the
386
+ unit name is not known.
387
+
388
+ Required arguments:
389
+
390
+ unit -- The unit name for which to return the primary unit.
391
+
392
+ """
393
+ primary = self._primary(unit)
394
+ if primary is None:
395
+ return None
396
+ return primary.name
397
+
398
+ def alternate_units(self, unit: str) -> list[str]:
399
+ """Return a list of available alternate units of a given unit.
400
+
401
+ In the returned list, the primary unit is listed first, the
402
+ rest is sorted alphabetically by unit name. If the specified
403
+ unit is not known, an empty list is returned.
404
+
405
+ Required arguments:
406
+
407
+ unit -- The unit for which to find the alternate units. This
408
+ does not need to be a primary unit. The specified unit is
409
+ also included in the returned list.
410
+
411
+ """
412
+ primary = self._primary(unit)
413
+ available = []
414
+ if primary is not None:
415
+ if primary.alternates:
416
+ for alt in primary.alternates.keys():
417
+ available.append(alt)
418
+ available.sort()
419
+ available.insert(0, primary.name)
420
+ return available
421
+
422
+ def scaled_units(self, unit: str) -> list[str]:
423
+ """Return a list of available scaled units.
424
+
425
+ The returned list is sorted by increasing scale factor.
426
+
427
+ unit -- The primary unit for which to find the available
428
+ scaled units. This unit is is not included in the
429
+ returned list.
430
+
431
+ """
432
+ pu = self._map.get(unit)
433
+ if not isinstance(pu, PrimaryUnit) or pu.scaled is None:
434
+ return []
435
+
436
+ available: list[tuple[str, float]] = []
437
+ for name, factor in pu.scaled.items():
438
+ available.append((name, factor))
439
+ available.sort(key=lambda el: el[1])
440
+ return [el[0] for el in available]
441
+
442
+ def _primary(self, unit: str) -> PrimaryUnit | None:
443
+ """Get the primary unit object for a unit.
444
+
445
+ Returns the primary unit object or `None` if the unit name is
446
+ not known.
447
+
448
+ Required arguments:
449
+
450
+ unit -- The unit name whose primary unit to get.
451
+
452
+ """
453
+ pu = self._map.get(unit)
454
+ if not isinstance(pu, PrimaryUnit):
455
+ return None
456
+ if isinstance(pu, AlternateUnit):
457
+ pu = pu.primary
458
+ return pu
459
+
460
+
461
+ def _well_scaled(x: float) -> bool:
462
+ """Check if number is "well scaled".
463
+
464
+ The number 0 is always well-scaled. All other numbers must have
465
+ an absolute value in the range from 1 to 1000 (exclusive) for this
466
+ to be the case.
467
+
468
+ Required arguments:
469
+
470
+ x -- The number to check for well-scaledness.
471
+
472
+ """
473
+ return x == 0 or (1.0 <= abs(x) < 1000)