Skip to content

Commit e6cbf68

Browse files
committed
Implement minimums by size as per CDI Standard.
1 parent 1a90cdc commit e6cbf68

3 files changed

Lines changed: 174 additions & 14 deletions

File tree

openlcb/cdivar.py

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,50 @@
1717
NUM_TYPES = {'int': int, 'float': float} # type: dict[str, Type]
1818
# Assumes "IEEE" in OpenLCB CDI Standard means IEEE 754-2008:
1919
FLOAT_MAXIMUMS = {2: 65504.0, 4: 3.40e38, 8: 1.80e308} # type: dict[int, float] # noqa: E501
20+
# Float minimums (https://en.wikipedia.org/wiki/IEEE_754):
21+
# 16-bit smallest normal 6.10×10−5 subnormal 5.96×10−8
22+
# 32-bit smallest normal 1.18×10−38 subnormal 1.40×10−45
23+
# 64-bit smallest normal 2.23×10−308 subnormal 4.94×10−324
24+
F_MIN_BITS = {
25+
2: [0] * 16,
26+
4: [0] * 32,
27+
8: [0] * 64,
28+
}
29+
30+
# Set bits for the most negative finite number
31+
for size, bits in F_MIN_BITS.items():
32+
bits[0] = 1 # Sign bit = 1 (negative)
33+
if size == 2: # binary16: 1 sign + 5 exp + 10 mant
34+
for i in range(1, 6): bits[i] = 1 # exp = 11110
35+
bits[5] = 0 # ← important: clear LSB of exponent
36+
for i in range(6, 16): bits[i] = 1 # mantissa all 1s
37+
elif size == 4: # binary32: 1 sign + 8 exp + 23 mant
38+
for i in range(1, 9): bits[i] = 1 # exp = 11111110
39+
bits[8] = 0 # ← clear LSB of exponent
40+
for i in range(9, 32): bits[i] = 1
41+
else: # binary64: 1 sign + 11 exp + 52 mant
42+
for i in range(1, 12): bits[i] = 1 # exp = 111...1110
43+
bits[11] = 0 # ← clear LSB of exponent
44+
for i in range(12, 64): bits[i] = 1
45+
46+
FLOAT_MINIMUMS = {} # F_MIN_DATA = {}
47+
48+
for k, bits in F_MIN_BITS.items():
49+
# Create traceable binary string (e.g. "0b000...001")
50+
bit_str = "0b" + "".join(map(str, bits))
51+
# Convert to integer then to bytes
52+
value = int(bit_str, 2)
53+
data_bytes = value.to_bytes(k, 'big')
54+
fmt = {2: ">e", 4: ">f", 8: ">d"}[k]
55+
# F_MIN_DATA[k] = data_bytes
56+
FLOAT_MINIMUMS[k] = struct.unpack(fmt, data_bytes)[0]
57+
# print(f"binary{k*8:2d} bits: {bit_str}")
58+
# print(f"binary{k*8:2d} value: {F_MIN_DATA[k]:.20e}\n")
59+
# results:
60+
# -65504.0
61+
# -3.4028234663852886e+38
62+
# -1.7976931348623157e+308
63+
2064
UNSIGNED_INT_MAXIMUMS = { # type: dict[int, int]
2165
1: 0xFF, 2: 0xFFFF, 4: 0xFFFF_FFFF, 8: 0xFFFF_FFFF_FFFF_FFFF}
2266
SIGNED_INT_MINIMUMS = {}
@@ -89,7 +133,8 @@ class CDIVar:
89133

90134
def __init__(self, className, _min=None, _max=None,
91135
_size=None, _default=None, assert_range=False,
92-
_no_min=False, _no_max=False, _default_data=None):
136+
_no_min=False, _no_max=False, _default_data=None,
137+
signed=None):
93138
self.data = None # type: bytes|None
94139
self.min = _min # type: CDIVar|None
95140
self.max = _max # type: CDIVar|None
@@ -101,8 +146,6 @@ def __init__(self, className, _min=None, _max=None,
101146
f"Expected {list(CLASSNAME_TYPES.keys())} got {className}"
102147
if _default is not None:
103148
assert isinstance(_default, CDIVar)
104-
if className in NUM_TYPES:
105-
_default.assertNumberFormat()
106149
assert _default_data is None, \
107150
"Can only set _default or _default_data"
108151
elif _default_data is not None:
@@ -113,9 +156,18 @@ def __init__(self, className, _min=None, _max=None,
113156
_no_max=True, _no_min=True) # prevent recursion
114157
_default.data = _default_data
115158

159+
if _default is not None:
160+
if className in NUM_TYPES:
161+
_default.assertNumberFormat()
162+
if _default < 0:
163+
if signed is None:
164+
signed = True
165+
116166
self.name = None # type: str|None
117167
self.className = className # type: str
118-
self.signed = False # type: bool
168+
if signed is None:
169+
signed = False
170+
self.signed = signed # type: bool
119171
assert isinstance(_no_min, bool)
120172
assert isinstance(_no_max, bool)
121173
self._no_min = _no_min
@@ -140,9 +192,26 @@ def __init__(self, className, _min=None, _max=None,
140192
raise AssertionError(error)
141193
else:
142194
logger.error(error)
143-
elif not _no_min:
144-
self.min = CDIVar(className, _size=_size,
145-
_no_min=True, _no_max=True) # prevent inf recurs
195+
elif (className in NUM_TYPES) and not _no_min:
196+
# self.min = CDIVar(className, _size=_size,
197+
# _no_min=True, _no_max=True) # prevent inf recurs
198+
# Set minimum based on size,
199+
# as per Configuration Description Information Standard.
200+
assert _size is not None
201+
if className == "int":
202+
if signed:
203+
# self.min.setInt(SIGNED_INT_MINIMUMS[_size])
204+
self.min = CDIVar.fromInt(SIGNED_INT_MINIMUMS[_size],
205+
_size)
206+
else:
207+
# self.min.setInt(0)
208+
self.min = CDIVar.fromInt(0, _size)
209+
elif className == "float":
210+
# self.min.setFloat(FLOAT_MINIMUMS[_size])
211+
self.min = CDIVar.fromFloat(FLOAT_MINIMUMS[_size], _size)
212+
else:
213+
raise NotImplementedError(f"no default minimum {className}")
214+
146215
if _max is not None:
147216
assert isinstance(_max, CDIVar)
148217
_max.assertNumberFormat()
@@ -342,9 +411,11 @@ def cmp_any(cls, self: 'CDIVar', other: Union['CDIVar', float, int],
342411
@classmethod
343412
def fromNumber(cls, value: Union[int, float],
344413
className: str, _size: int) -> 'CDIVar':
345-
var = CDIVar(className, _size=_size)
414+
var = CDIVar(className, _size=_size, _no_min=True)
415+
# ^ _no_min prevents infinite recursion generating min
346416
if value < 0:
347417
var.signed = True
418+
var.min = None # remove default (0)
348419
if className == "int":
349420
assert isinstance(value, int)
350421
var.setInt(value)
@@ -484,6 +555,7 @@ def intToData(self, value: int) -> bytes:
484555
try:
485556
return struct.pack(self.packFormat(), value)
486557
except Exception as ex:
558+
logger.error("")
487559
logger.error(formatted_ex(ex))
488560
logger.error(
489561
f"Tried to set a(n) {self.subtype()} CDIVar"

openlcb/storagepool.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import struct
33
from typing import Union
44

5-
from openlcb.cdivar import SUBTYPE_FORMATS
5+
from openlcb.cdivar import SUBTYPE_FORMATS, CDIVar
66
from openlcb.memoryspace import MemorySpace
77

88
logger = getLogger(__name__)
@@ -12,17 +12,42 @@ class StoragePool:
1212
def __init__(self):
1313
self.spaces = {} # type: dict[int, bytearray]
1414

15+
def set(self, var: CDIVar):
16+
assert isinstance(var, CDIVar)
17+
assert var.space is not None
18+
assert var.address
19+
data = var.getData()
20+
assert data is not None
21+
self.setData(var.space, var.address, data, size=var.size)
22+
23+
def get(self, var: CDIVar) -> CDIVar:
24+
"""Modify var in place.
25+
26+
Returns:
27+
CDIVar: Same var instance (returned by reference) modified.
28+
"""
29+
assert isinstance(var, CDIVar)
30+
assert var.space is not None
31+
assert var.address
32+
assert var.size is not None
33+
data = self.getData(var.space, var.address, var.size)
34+
assert data is not None
35+
var.setData(data)
36+
return var
37+
1538
def setData(self, space: Union[MemorySpace, int], address: int,
16-
data: Union[bytes, bytearray]):
39+
data: Union[bytes, bytearray], size=None):
1740
"""Set address in virtual memory space to data"""
1841
assert isinstance(data, (bytearray, bytes))
1942
if isinstance(space, MemorySpace):
2043
space = space.value
2144
assert isinstance(space, int)
2245
assert isinstance(address, int)
2346
assert address >= 0
24-
# if size is None:
25-
size = len(data)
47+
if size is None:
48+
size = len(data)
49+
else:
50+
assert size <= len(data)
2651
end = address + size
2752

2853
if space not in self.spaces:
@@ -37,7 +62,10 @@ def setData(self, space: Union[MemorySpace, int], address: int,
3762
f" byte(s) to {end} byte(s).")
3863
self.spaces[space] += b'\0' * newRegionLen
3964
assert end - address == len(data)
40-
self.spaces[space][address:end] = data
65+
if size < len(data):
66+
self.spaces[space][address:end] = data[:size]
67+
else:
68+
self.spaces[space][address:end] = data
4169

4270
def getData(self, space: Union[MemorySpace, int], address: int,
4371
size: int, force=False) -> bytearray:
@@ -73,7 +101,6 @@ def setInt(self, space: Union[MemorySpace, int], address: int,
73101
typeStr = f"int{size*8}"
74102
dataFormat = SUBTYPE_FORMATS[typeStr]
75103
data = struct.pack(dataFormat, value)
76-
print(f"packing {dataFormat}")
77104
assert len(data) == size, \
78105
f"Expected {size} byte(s) for {typeStr}, got {len(data)}"
79106
return self.setData(space, address, data)

tests/test_storagepool.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import sys
44
import unittest
55

6+
from openlcb import emit_cast
7+
from openlcb.cdivar import SIGNED_INT_MINIMUMS, CDIVar
68
from openlcb.storagepool import StoragePool
79

810

@@ -130,6 +132,65 @@ def testFloat(self):
130132
out_value = pool.getFloat(1, 10, size)
131133
self.assertEqual(in_value, out_value)
132134

135+
def testCDIVarUInt(self):
136+
size = 4
137+
var = CDIVar("int", _size=size)
138+
var.space = 1
139+
var.address = 10
140+
in_value = 999
141+
# signed = False
142+
var.setInt(in_value)
143+
self.assertEqual(var.getInt(), in_value)
144+
pool = StoragePool()
145+
pool.set(var)
146+
var = pool.get(var)
147+
self.assertEqual(var.getInt(), in_value)
148+
assert var.space is not None
149+
assert var.address is not None
150+
assert var.size is not None
151+
assert var.signed is not None
152+
out_value = pool.getInt(var.space, var.address, var.size, var.signed)
153+
self.assertEqual(out_value, in_value)
154+
155+
def testCDIVarSInt(self):
156+
size = 4
157+
in_value = -999
158+
signed = True if in_value < 0 else False
159+
# defaultVar = CDIVar("int", _size=size, _no_min=True, _no_max=True,
160+
# signed=signed)
161+
# defaultVar.setInt(in_value)
162+
# simplified construction:
163+
defaultVar = CDIVar.fromInt(in_value, size)
164+
self.assertTrue(defaultVar.signed)
165+
self.assertIsNone(defaultVar.min)
166+
var = CDIVar(
167+
"int",
168+
_size=size,
169+
_default=defaultVar, # forces signed since negative
170+
)
171+
self.assertTrue(var.signed)
172+
self.assertIsInstance(var.min, CDIVar)
173+
self.assertIsNotNone(
174+
var.min,
175+
msg=f"{emit_cast(var.min)} should be min for {size*8}-bit")
176+
self.assertEqual(var.min, SIGNED_INT_MINIMUMS[size])
177+
# ^ == allowed since __eq__ is defined for CDIVar (var.min)
178+
var.space = 1
179+
var.address = 10
180+
# signed = False
181+
var.setInt(in_value)
182+
self.assertEqual(var.getInt(), in_value)
183+
pool = StoragePool()
184+
pool.set(var)
185+
var = pool.get(var)
186+
self.assertEqual(var.getInt(), in_value)
187+
assert var.space is not None
188+
assert var.address is not None
189+
assert var.size is not None
190+
assert var.signed is True
191+
out_value = pool.getInt(var.space, var.address, var.size, var.signed)
192+
self.assertEqual(out_value, in_value)
193+
133194

134195
if __name__ == "__main__":
135196
unittest.main()

0 commit comments

Comments
 (0)