|
1 | 1 |
|
2 | | -import json |
3 | | -import math |
| 2 | +import struct |
4 | 3 |
|
5 | 4 | from openlcb import emit_cast |
6 | | -from typing import Any, Type |
| 5 | +from typing import List, Type, Union |
7 | 6 |
|
8 | 7 | from openlcb.eventid import EventID |
9 | 8 | from openlcb.openlcbaction import OpenLCBAction |
|
15 | 14 | CLASSNAME_TYPES = {'int': int, 'float': float, 'string': str, |
16 | 15 | 'blob': bytearray, 'eventid': EventID, |
17 | 16 | 'action': OpenLCBAction} |
| 17 | +SUBTYPE_FORMATS = { |
| 18 | + 'int8': "b", 'uint8': "B", |
| 19 | + 'int16': ">h", 'uint16': ">H", |
| 20 | + 'int32': ">i", 'uint32': ">I", |
| 21 | + 'int64': ">q", 'uint64': ">Q", |
| 22 | + 'float16': ">e", |
| 23 | + 'float32': ">f", |
| 24 | + 'float64': ">d", |
| 25 | +} |
| 26 | +STANDARD_SIZES = { |
| 27 | + 'int': (1, 2, 4, 8), |
| 28 | + 'float': (2, 4, 8), |
| 29 | +} |
18 | 30 |
|
19 | 31 |
|
20 | 32 | class CDIVar: |
21 | | - """ |
| 33 | + """A byte array representing a single configuration variable. |
| 34 | + Arguments: |
| 35 | + _default (bytearray): An array with length matching size. |
| 36 | + _min (int): Minimum value (only for int/float className. <0 sets |
| 37 | + .signed = True) |
| 38 | + _max (int): Maximum value (only for int/float className) |
| 39 | + _size (int): Size of int/float (not allowed for other className). |
| 40 | +
|
22 | 41 | Attributes: |
| 42 | + className (str): An OpenLCB CDI type. Must be a key in |
| 43 | + CLASSNAME_TYPES. |
23 | 44 | floatFormat (str): Optional printf-style format |
24 | 45 | (for className == "float"). |
25 | 46 | signed (bool): Whether the value is signed (False unless min is |
26 | 47 | negative). Defaults to True. |
27 | 48 | See LCC "Configuration Description Information" Standard. |
28 | | - value (Any): The value read from the device (type should be |
29 | | - from CLASSNAME_TYPES values). |
| 49 | + _data (bytes): The value read from the device or ready to |
| 50 | + write. Only None if not read yet, otherwise length |
| 51 | + must be .size. |
30 | 52 | """ |
31 | 53 | TYPED_KEYS = ['min', 'max', 'default'] |
32 | 54 |
|
33 | | - def __init__(self, className): |
| 55 | + def __init__(self, className, _min=None, _max=None, |
| 56 | + _size=None, _default=None): |
34 | 57 | assert isinstance(className, str), \ |
35 | 58 | f"Expected {CLASSNAME_TYPES.keys()} got {emit_cast(className)}" |
36 | 59 | assert className, f"Expected {CLASSNAME_TYPES.keys()} got {className}" |
37 | 60 | assert className in CLASSNAME_TYPES, \ |
38 | | - f"Expected {CLASSNAME_TYPES.keys()} got {className}" |
| 61 | + f"Expected {list(CLASSNAME_TYPES.keys())} got {className}" |
39 | 62 | self.className = className # type: str |
| 63 | + self.data = None # type: bytes|None |
| 64 | + self.min = _min # type: int|float|None |
40 | 65 | self.signed = False # type: bool |
41 | | - self.value = None # type: Any |
42 | | - self.min = None # type: int|float|None |
43 | | - self.max = None # type: int|float|None |
44 | | - self.default = None # type: int|float|None |
45 | | - self.size = None # type: int|None |
| 66 | + if self.min and self.min < 0: |
| 67 | + self.signed = True |
| 68 | + self.max = _max # type: int|float|None |
| 69 | + self.default = _default # type: bytearray|None |
| 70 | + self.size = _size # type: int|None |
| 71 | + if self.size is None: |
| 72 | + if self.default is not None: |
| 73 | + self.size = len(self.default) |
| 74 | + if self.className in ("int", "float"): |
| 75 | + self.assertNumberFormat() |
46 | 76 | self.floatFormat = None # type: str|None |
| 77 | + |
| 78 | + def isNumber(self): |
| 79 | + return self.className in ("int", "float") |
| 80 | + |
| 81 | + def standardSizes(self) -> Union[List[int], None]: |
| 82 | + return STANDARD_SIZES.get(self.className) |
| 83 | + |
| 84 | + def assertNumberFormat(self, assertWhat=""): |
| 85 | + if self.className == "int": |
| 86 | + assert self.size in (1, 2, 4, 8) |
| 87 | + elif self.className == "float": |
| 88 | + assert self.size in (2, 4, 8) |
| 89 | + else: |
| 90 | + if not assertWhat: |
| 91 | + assertWhat = f"Expected float/int size {STANDARD_SIZES}" |
| 92 | + raise TypeError( |
| 93 | + f"{assertWhat}" |
| 94 | + f", but cdivar is {self.className} size={self.size}") |
| 95 | + |
| 96 | + def bitDepth(self) -> int: |
| 97 | + self.assertNumberFormat(assertWhat="Only float/int has bitDepth") |
| 98 | + return self.size * 8 # type:ignore (assert precludes bad size) |
| 99 | + |
| 100 | + def subtype(self) -> str: |
| 101 | + """Get the number subtype in C++-like notation. |
| 102 | +
|
| 103 | + Returns: |
| 104 | + str: Key for SUBTYPE_FORMATS. |
| 105 | +
|
| 106 | + Raises: |
| 107 | + TypeError: (raised by bitDepth) if not int 8-64 bit, and not |
| 108 | + float 16-64 bit. |
| 109 | + """ |
| 110 | + prefix = "" |
| 111 | + if self.className == "int" and not self.signed: |
| 112 | + prefix = "u" |
| 113 | + return f"{prefix}{self.className}{self.bitDepth()}" |
| 114 | + |
| 115 | + def packFormat(self) -> str: |
| 116 | + assert self.className in ("int", "float"), \ |
| 117 | + f"Can only pack if isNumber, but this cdivar is {self.className}" |
| 118 | + return SUBTYPE_FORMATS[self.subtype()] |
| 119 | + |
| 120 | + def intToData(self, value: int) -> bytes: |
| 121 | + assert self.className == "int" |
| 122 | + assert isinstance(value, int) |
| 123 | + return struct.pack(self.packFormat(), value) |
| 124 | + |
| 125 | + def setInt(self, value: int): |
| 126 | + self.data = self.intToData(value) |
| 127 | + |
| 128 | + def floatToData(self, value: float) -> bytes: |
| 129 | + assert self.className == "float" |
| 130 | + assert isinstance(value, float) |
| 131 | + return struct.pack(self.packFormat(), value) |
| 132 | + |
| 133 | + def setFloat(self, value: float): |
| 134 | + self.data = self.floatToData(value) |
| 135 | + |
| 136 | + def stringToData(self, value: str) -> bytes: |
| 137 | + assert self.className == "string" |
| 138 | + assert isinstance(value, str) |
| 139 | + return value.encode("utf-8") |
| 140 | + |
| 141 | + def setString(self, value: str): |
| 142 | + self.data = self.stringToData(value) |
| 143 | + self.size = len(self.data) |
| 144 | + |
| 145 | + def dataToInt(self, data) -> Union[int, None]: |
| 146 | + assert self.className == "int" |
| 147 | + if (data is None) or (len(data) < 1): |
| 148 | + return None |
| 149 | + assert self.size == len(data) |
| 150 | + # [0] since always returns list (and there is only one as per |
| 151 | + # Standard and the assertion above): |
| 152 | + return struct.unpack(self.packFormat(), data)[0] |
| 153 | + |
| 154 | + def getInt(self) -> Union[int, None]: |
| 155 | + return self.dataToInt(self.data) |
| 156 | + |
| 157 | + def dataToFloat(self, data) -> Union[float, None]: |
| 158 | + assert self.className == "float" |
| 159 | + if (data is None) or (len(data) < 1): |
| 160 | + return None |
| 161 | + assert self.size == len(data) |
| 162 | + # [0] since always returns list (and there is only one as per |
| 163 | + # Standard and the assertion above): |
| 164 | + return struct.unpack(self.packFormat(), data)[0] |
| 165 | + |
| 166 | + def getFloat(self) -> Union[float, None]: |
| 167 | + return self.dataToFloat(self.data) |
| 168 | + |
| 169 | + def dataToString(self, data) -> Union[str, None]: |
| 170 | + assert self.className == "string" |
| 171 | + if (data is None) or (len(data) < 1): |
| 172 | + return None |
| 173 | + return data.decode("utf-8") |
| 174 | + |
| 175 | + def getString(self) -> Union[str, None]: |
| 176 | + return self.dataToString(self.data) |
0 commit comments