Skip to content

Commit 123833a

Browse files
committed
Complete the CDIVar backend and add tests.
1 parent b72de03 commit 123833a

2 files changed

Lines changed: 227 additions & 13 deletions

File tree

openlcb/cdivar.py

Lines changed: 143 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11

2-
import json
3-
import math
2+
import struct
43

54
from openlcb import emit_cast
6-
from typing import Any, Type
5+
from typing import List, Type, Union
76

87
from openlcb.eventid import EventID
98
from openlcb.openlcbaction import OpenLCBAction
@@ -15,32 +14,163 @@
1514
CLASSNAME_TYPES = {'int': int, 'float': float, 'string': str,
1615
'blob': bytearray, 'eventid': EventID,
1716
'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+
}
1830

1931

2032
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+
2241
Attributes:
42+
className (str): An OpenLCB CDI type. Must be a key in
43+
CLASSNAME_TYPES.
2344
floatFormat (str): Optional printf-style format
2445
(for className == "float").
2546
signed (bool): Whether the value is signed (False unless min is
2647
negative). Defaults to True.
2748
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.
3052
"""
3153
TYPED_KEYS = ['min', 'max', 'default']
3254

33-
def __init__(self, className):
55+
def __init__(self, className, _min=None, _max=None,
56+
_size=None, _default=None):
3457
assert isinstance(className, str), \
3558
f"Expected {CLASSNAME_TYPES.keys()} got {emit_cast(className)}"
3659
assert className, f"Expected {CLASSNAME_TYPES.keys()} got {className}"
3760
assert className in CLASSNAME_TYPES, \
38-
f"Expected {CLASSNAME_TYPES.keys()} got {className}"
61+
f"Expected {list(CLASSNAME_TYPES.keys())} got {className}"
3962
self.className = className # type: str
63+
self.data = None # type: bytes|None
64+
self.min = _min # type: int|float|None
4065
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()
4676
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)

tests/test_cdivar.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import unittest
2+
from openlcb.cdivar import CDIVar, SUBTYPE_FORMATS
3+
4+
5+
class TestCDIVar(unittest.TestCase):
6+
7+
def test_initialization_valid(self):
8+
cdivar_int = CDIVar(className='int', _min=0, _max=100, _size=4,
9+
_default=bytearray(b'\x00\x00\x00\x00'))
10+
self.assertEqual(cdivar_int.className, 'int')
11+
self.assertEqual(cdivar_int.min, 0)
12+
self.assertEqual(cdivar_int.max, 100)
13+
self.assertEqual(cdivar_int.size, 4)
14+
15+
cdivar_float = CDIVar(className='float', _min=0.0, _max=100.0, _size=4)
16+
self.assertEqual(cdivar_float.className, 'float')
17+
self.assertEqual(cdivar_float.min, 0.0)
18+
self.assertEqual(cdivar_float.max, 100.0)
19+
self.assertEqual(cdivar_float.size, 4)
20+
21+
cdivar_string = CDIVar(className='string',
22+
_default=bytearray(b'Hello'))
23+
self.assertEqual(cdivar_string.className, 'string')
24+
self.assertEqual(cdivar_string.default, bytearray(b'Hello'))
25+
assert cdivar_string.default is not None
26+
self.assertEqual(cdivar_string.size, len(cdivar_string.default))
27+
28+
def test_initialization_invalid_classname(self):
29+
with self.assertRaises(AssertionError):
30+
CDIVar(className='invalid_class')
31+
32+
def test_subtype(self):
33+
cdivar_signed_int = CDIVar(className='int', _size=4, _min=-100,
34+
_max=100)
35+
self.assertEqual(cdivar_signed_int.subtype(), 'int32')
36+
cdivar_unsigned_int = CDIVar(className='int', _size=4)
37+
self.assertEqual(cdivar_unsigned_int.subtype(), 'uint32')
38+
cdivar_signed_float = CDIVar(className='float', _size=4)
39+
self.assertEqual(cdivar_signed_float.subtype(), 'float32')
40+
41+
def test_pack_format(self):
42+
cdivar_int = CDIVar(className='int', _size=4)
43+
self.assertEqual(cdivar_int.packFormat(),
44+
SUBTYPE_FORMATS[cdivar_int.subtype()])
45+
46+
cdivar_float = CDIVar(className='float', _size=4)
47+
self.assertEqual(cdivar_float.packFormat(),
48+
SUBTYPE_FORMATS[cdivar_float.subtype()])
49+
50+
def test_set_get_int(self):
51+
cdivar_int = CDIVar(className='int', _size=4)
52+
cdivar_int.setInt(42)
53+
self.assertEqual(cdivar_int.getInt(), 42)
54+
55+
def test_set_get_float(self):
56+
cdivar_float = CDIVar(className='float', _size=4)
57+
cdivar_float.setFloat(3.14)
58+
got = cdivar_float.getFloat()
59+
assert got is not None
60+
self.assertAlmostEqual(got, 3.14, places=6)
61+
62+
def test_set_get_string(self):
63+
cdivar_string = CDIVar(className='string')
64+
cdivar_string.setString("Hello")
65+
self.assertEqual(cdivar_string.getString(), "Hello")
66+
67+
def test_invalid_set_int(self):
68+
cdivar_int = CDIVar(className='int', _size=4)
69+
with self.assertRaises(AssertionError):
70+
cdivar_int.setInt("not an int") # type:ignore (assertRaises)
71+
72+
def test_invalid_set_float(self):
73+
cdivar_float = CDIVar(className='float', _size=4)
74+
with self.assertRaises(AssertionError):
75+
cdivar_float.setFloat("not a float") # type:ignore (assertRaises)
76+
77+
def test_invalid_set_string(self):
78+
cdivar_string = CDIVar(className='string')
79+
with self.assertRaises(AssertionError):
80+
cdivar_string.setString(12345) # type:ignore (assertRaises)
81+
82+
83+
if __name__ == '__main__':
84+
unittest.main()

0 commit comments

Comments
 (0)