Skip to content

Commit 9569133

Browse files
committed
Move bitfields to MemoryConfigurationHeader class and MemorySpaceIndex enum to make usage and enforcement more clear.
1 parent 4a3283c commit 9569133

5 files changed

Lines changed: 162 additions & 87 deletions

File tree

openlcb/convert.py

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,45 +19,6 @@
1919

2020
class Convert:
2121

22-
@staticmethod
23-
def deserializeMC2ndByte(datagramByte1):
24-
"""Decode byte[1] (2nd) of Memory Configuration Datagram"""
25-
has_byte6 = False
26-
if datagramByte1 & 0x03 == 0:
27-
has_byte6 = True
28-
return has_byte6, datagramByte1 & 0xFC
29-
# ^ 0xFC = 11111100
30-
31-
# formerly spaceDecode, but it serializes a space for datagram byte2
32-
@staticmethod
33-
def serializeSpace(space):
34-
"""Convert from a space number to either
35-
False and control number or True and standard memory space
36-
for use in a Datagram.
37-
38-
Args:
39-
space (int): Sequential memory space identifier, where values:
40-
- 0xFF to 0xFD are special spaces, and only the least significant
41-
2 bits will be used in a datagram.
42-
- 0x00 to 0xFC represent standard memory spaces directly.
43-
44-
Returns:
45-
tuple(bool, byte): (is custom space, control | space)
46-
- (False, control number 1 to 3 inclusive) :
47-
spaces 0xFF - 0xFD (Except bits beyond 0x00000011
48-
differ for each datagram type. See 4.2 Address
49-
Space Selection in OpenLCB Memory Configuration
50-
Standard)
51-
- or (True, space number) : spaces 0 - 0xFC
52-
(NOTE: type of space may affect type of output)
53-
"""
54-
# TODO: Maybe check type of space & raise TypeError if not
55-
# something valid, whether byte, int, or what is ok [add
56-
# more _description_ to space in docstring].
57-
if space >= 0xFD:
58-
return (False, space & 0x03)
59-
return (True, space)
60-
6122
@staticmethod
6223
def arrayToInt(data: Union[bytes, bytearray, List[int]]) -> int:
6324
"""Convert an array in MSB-first order to an integer
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
2+
from enum import Enum
3+
from typing import Union
4+
# FDI = 0xFA
5+
# Configuration = 0xFD
6+
# All = 0xFE
7+
# CDI = 0xFF # ~~decodes~~ encoded (in header) as 0x03
8+
9+
10+
class MemorySpaceIndex(Enum):
11+
Uninitialized = -1
12+
Custom = 0
13+
Configuration = 1 # 0xFD & 0x03 == 1
14+
All = 2 # 0xFE & 0x03 == 2
15+
CDI = 3 # 0xFF & 0x03 == 3
16+
17+
@classmethod
18+
def fromNumber(cls, num: int):
19+
"""Return the MemorySpace member with the given numeric value,
20+
or None if no match is found.
21+
"""
22+
assert isinstance(num, int)
23+
for member in cls:
24+
if member.value == num:
25+
return member
26+
return cls.Custom
27+
28+
29+
class MemoryConfigurationHeader:
30+
"""Manage data corresponding to bitfields in Memory Configuration.
31+
See OpenLCB "Memory Configuration" Standard
32+
33+
Arguments:
34+
space (int): Space number (MemorySpaceIndex.*.Value,
35+
MemorySpace.*.value, or raw number including a custom
36+
space).
37+
- 0xFF to 0xFD are special spaces, and only the least
38+
significant 2 bits will be used in a datagram.
39+
- 0x00 to 0xFC represent standard memory spaces directly.
40+
"""
41+
def __init__(self, space: int):
42+
# formerly Convert.serializeSpace
43+
# formerly spaceDecode, but it serializes a space for datagram byte2
44+
assert isinstance(space, int)
45+
spaceIndexValue = space & 0x03
46+
self.spaceIndex = \
47+
MemorySpaceIndex.fromNumber(
48+
spaceIndexValue) # type: MemorySpaceIndex
49+
self.customSpace = None # type: int|None
50+
if self.spaceIndex is MemorySpaceIndex.Custom:
51+
self.customSpace = space
52+
self.highBits = 0 # type: int
53+
54+
@classmethod
55+
def fromMC2ndByte(cls, datagramByte1: int, space: Union[int, None] = None) -> 'MemoryConfigurationHeader': # noqa: E501
56+
"""Deserialize Memory Configuration byte 1.
57+
58+
For serializing a space (such as packing a datagram header),
59+
use constructor instead.
60+
61+
Args:
62+
datagramByte1 (int): byte[1] (2nd) of Memory Configuration Datagram
63+
space (int): Only applies for custom space (datagramByte)
64+
"""
65+
if space is not None:
66+
assert isinstance(space, int)
67+
assert datagramByte1 & 0x03 == 0, \
68+
'custom space requires datagramByte1 with last 2 bits 00'
69+
else:
70+
# space is None
71+
assert datagramByte1 & 0x03 != 0, \
72+
'a standard space must be in last 2 bits datagramByte1'
73+
space = -1
74+
# formerly deserializeMC2ndByte
75+
result = cls(datagramByte1 & 0x03)
76+
if datagramByte1 & 0x03 == 0:
77+
# Default (-1) means not enough information
78+
# (space not known, but is MemorySpaceIndex.Custom)
79+
result.customSpace = space
80+
result.highBits = datagramByte1 & 0xFC # 0xFC = 0b11111100
81+
return result

openlcb/memoryservice.py

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -40,23 +40,30 @@
4040
DatagramService,
4141
)
4242
from openlcb.convert import Convert
43+
from openlcb.memoryconfigurationheader import MemoryConfigurationHeader, MemorySpaceIndex
44+
from openlcb.memorymanager import MemoryManager
4345
from openlcb.nodeid import NodeID
4446

4547
logger = getLogger(__name__)
4648

47-
49+
MODE_BYTES['']
4850
MODE_BYTES = {
49-
'Read_Command': {0x40, 0x41, 0x42, 0x43},
51+
# order determines meaning for lists (See )
52+
'Read_Command': {0x40, 0x41, 0x42, 0x43}, # TODO: Use memoryManagers
5053
'Read_Reply': {0x50, 0x51, 0x52, 0x53},
51-
'Read_Stream_Command': {0x60, 0x61, 0x62, 0x63},
52-
'Read_Stream_Reply': {0x70, 0x71, 0x72, 0x73},
53-
'Write_Command': {0x00, 0x01, 0x02, 0x03},
54+
'Read_Stream_Command': {0x60, 0x61, 0x62, 0x63}, # TODO: Use memoryManagers
55+
'Read_Stream_Reply': {0x70, 0x71, 0x72, 0x73}, # TODO
56+
'Write_Command': [0x00, 0x01, 0x02, 0x03], # TODO: Use memoryManagers
5457
'Write_Reply': {0x10, 0x11, 0x12, 0x13},
55-
'Write_Under_Mask_Command': {0x08, 0x09, 0x0A, 0x0B},
58+
'Write_Under_Mask_Command': {0x08, 0x09, 0x0A, 0x0B}, # TODO: Use memoryManagers
5659
'Write_Stream_Command': {0x20, 0x21, 0x22, 0x23},
57-
'Write_Stream_Reply': {0x30, 0x31, 0x32, 0x33},
60+
'Write_Stream_Reply': {0x30, 0x31, 0x32, 0x33}, # TODO
61+
'Get_Address_Space_Info_Command': {0x84, },
62+
'Get_Address_Space_Info_Reply': {0x86, 0x87, },
63+
'Lock_Reserve_Command': {0x88, },
5864
}
5965

66+
6067
MODE_ERROR_BYTES = {
6168
'Read_Reply': {0x58, 0x59, 0x5A, 0x5B},
6269
'Read_Stream_Reply': {0x78, 0x79, 0x7A, 0x7B},
@@ -85,10 +92,10 @@ class MemorySpace(Enum):
8592
(See OpenLCB Memory Configuration Standard 4.2).
8693
"""
8794
Uninitialized = -1
88-
CDI = 0xFF # decodes to 0x03
8995
FDI = 0xFA
90-
All = 0xFE
9196
Configuration = 0xFD
97+
All = 0xFE
98+
CDI = 0xFF # decodes to 0x03
9299

93100
@classmethod
94101
def fromNumber(cls, num: int):
@@ -101,6 +108,22 @@ def fromNumber(cls, num: int):
101108
return member
102109
return None
103110

111+
@classmethod
112+
def fromIndex(cls, msi: MemorySpaceIndex):
113+
"""Return the MemorySpace member with the given numeric value,
114+
or None if no match is found.
115+
"""
116+
assert isinstance(msi, MemorySpaceIndex)
117+
if msi is MemorySpaceIndex.Custom:
118+
return None
119+
elif msi is MemorySpaceIndex.Configuration:
120+
return cls.Configuration
121+
elif msi is MemorySpaceIndex.All:
122+
return cls.All
123+
elif msi is MemorySpaceIndex.CDI:
124+
return cls.CDI
125+
return None
126+
104127

105128
class MemoryReadMemo:
106129
"""Memo carries request and reply.
@@ -211,10 +234,16 @@ def parseReplyDatagram(memo: Union[MemoryReadMemo, MemoryWriteMemo],
211234
"Datagram is truncated to 1 byte:"
212235
f" it is {hex(dmemo.data[0])}")
213236
return
214-
(hasByte6, _) = Convert.deserializeMC2ndByte(dmemo.data[1])
237+
mcHeader = MemoryConfigurationHeader.fromMC2ndByte(
238+
dmemo.data[1],
239+
# space=memo.space,
240+
)
215241
offset = 6
216242
error = None
217-
if hasByte6:
243+
assert mcHeader.spaceIndex is not MemorySpaceIndex.Uninitialized
244+
if mcHeader.spaceIndex is MemorySpaceIndex.Custom:
245+
# mcHeader.customSpace = memo.space
246+
mcHeader.customSpace = dmemo.data[6]
218247
offset = 7
219248
memo.error = None
220249
memo.errorCode = None
@@ -256,9 +285,15 @@ def parseReplyDatagram(memo: Union[MemoryReadMemo, MemoryWriteMemo],
256285
error += f" ({list(dmemo.data[message_idx:])})"
257286
else:
258287
error = f"(2nd byte = {hex(dmemo.data[1])})"
259-
error += f" (hasByte6={hasByte6})"
260-
if hasByte6:
261-
error += f" (space={hex(dmemo.data[6])})"
288+
error += f" (spaceIndex={mcHeader.spaceIndex})"
289+
if mcHeader.spaceIndex is mcHeader.customSpace:
290+
if mcHeader.customSpace is not None:
291+
if mcHeader.customSpace != dmemo.data[6]:
292+
error += f" (mcHeader.customSpace={hex(mcHeader.customSpace)} != space={hex(dmemo.data[6])} !)" # noqa: E501
293+
else:
294+
error += f" (mcHeader.customSpace={hex(mcHeader.customSpace)})"
295+
else:
296+
error += f" (space={hex(dmemo.data[6])} mcHeader.customSpace=None!)" # noqa: E501
262297
memo.error = error
263298

264299

@@ -268,6 +303,12 @@ class MemoryService:
268303
269304
Args:
270305
service (DatagramService): See DatagramService.
306+
307+
Attributes:
308+
memoryManagers (dict[str, MemoryManager]): The storage where
309+
other nodes can read and write memory. Each element can be
310+
changed to a specific nodeid's memory manager. They key is
311+
the NodeID in string form (dotted notation).
271312
"""
272313

273314
def __init__(self, service: DatagramService):
@@ -280,6 +321,7 @@ def __init__(self, service: DatagramService):
280321
self.service.registerDatagramReceivedListener(
281322
self.datagramReceivedListener
282323
)
324+
self.memoryManagers = {} # type: dict[str, MemoryManager]
283325

284326
def requestMemoryRead(self, memo, stream: bool = False):
285327
# type: (MemoryReadMemo, Optional[bool]) -> None
@@ -308,16 +350,10 @@ def requestMemoryReadNext(self, memo, stream: bool = False):
308350
memo (MemoryReadMemo): Request to send.
309351
"""
310352
assert isinstance(stream, bool)
311-
hasByte6 = False # if custom space is defined in byte 6
312-
flag = 0
313-
(hasByte6, flag) = Convert.serializeSpace(memo.space)
314-
if stream:
315-
# Encoding: 0x60=custom, 0x61=0xFD, 0x62=0xFE, 0x63=0xFF
316-
spaceFlag = 0x60 if hasByte6 else (flag | 0x60)
317-
else:
318-
# Encoding: 0x40=custom, 0x41=0xFD, 0x42=0xFE, 0x43=0xFF
319-
spaceFlag = 0x40 if hasByte6 else (flag | 0x40) # | 0b11111100
320-
# ^ In else case, flag is 1-3, so re-add 0xFC (0b11111100)
353+
mcHeader = MemoryConfigurationHeader(memo.space)
354+
assert mcHeader.spaceIndex is not None
355+
spaceFlag = (0x60 if stream else 0x40) | mcHeader.spaceIndex.value
356+
# NOTE: Why was there commented: | 0xFC (0b11111100) if not stream?
321357
addr2 = ((memo.address >> 24) & 0xFF)
322358
addr3 = ((memo.address >> 16) & 0xFF)
323359
addr4 = ((memo.address >> 8) & 0xFF)
@@ -326,7 +362,7 @@ def requestMemoryReadNext(self, memo, stream: bool = False):
326362
DatagramService.ProtocolID.MemoryOperation.value, spaceFlag,
327363
addr2, addr3, addr4, addr5])
328364
# NOTE: list[int] is ok for bytearray extend (`+` requires cast)
329-
if hasByte6:
365+
if mcHeader.customSpace is not None:
330366
assert memo.space <= 0xFF, f"Space {memo.space} out of byte range"
331367
data.extend([(memo.space & 0xFF)])
332368
data.extend([memo.size])
@@ -352,7 +388,6 @@ def datagramReceivedListener(self, dmemo: DatagramReadMemo) -> bool:
352388
if self.service.datagramType(dmemo.data) \
353389
!= DatagramService.ProtocolID.MemoryOperation :
354390
return False
355-
356391
# datagram must has a command value
357392
if len(dmemo.data) < 2:
358393
logger.error("Memory service datagram too short: {}"
@@ -449,14 +484,8 @@ def requestMemoryWrite(self, memo: MemoryWriteMemo, stream: bool = False):
449484
self.writeMemos.append(memo)
450485
# create & send a write datagram
451486
hasByte6 = False # if custom space is defined in byte 6
452-
flag = 0
453-
(hasByte6, flag) = Convert.serializeSpace(memo.space)
454-
if stream:
455-
# Encoding: 0x20=custom, 0x21=0xFD, 0x22=0xFE, 0x23=0xFF
456-
spaceFlag = 0x20 if hasByte6 else (flag | 0x20)
457-
else:
458-
# Encoding: 0x00=custom, 0x01=0xFD, 0x02=0xFE, 0x03=0xFF
459-
spaceFlag = 0x00 if hasByte6 else (flag | 0x00)
487+
header = MemoryConfigurationHeader(memo.space)
488+
spaceFlag = (0x20 if stream else 0) | header.spaceIndex.value
460489
addr2 = ((memo.address >> 24) & 0xFF)
461490
addr3 = ((memo.address >> 16) & 0xFF)
462491
addr4 = ((memo.address >> 8) & 0xFF)

python-openlcb.code-workspace

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
"localoverrides",
6969
"MDNS",
7070
"mdnsconventions",
71+
"memoryconfigurationheader",
7172
"memorymanager",
7273
"memoryservice",
7374
"metas",

tests/test_convert.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import unittest
44

55
from openlcb.convert import Convert
6+
from openlcb.memoryconfigurationheader import MemoryConfigurationHeader, MemorySpaceIndex
67

78

89
class TestConvertClass(unittest.TestCase):
@@ -88,17 +89,19 @@ def testIntToArrayFail(self):
8889
Convert.intToArray(value, length)
8990

9091
def testSerializeSpace(self):
91-
byte6 = False
92-
space = 0x00
93-
94-
(byte6, space) = Convert.serializeSpace(0xF8)
95-
self.assertEqual(space, 0xF8)
96-
self.assertTrue(byte6)
97-
98-
(byte6, space) = Convert.serializeSpace(0xFF)
99-
self.assertEqual(space, 0x03)
100-
self.assertFalse(byte6)
101-
102-
(byte6, space) = Convert.serializeSpace(0xFD)
103-
self.assertEqual(space, 0x01)
104-
self.assertFalse(byte6)
92+
# byte6 = False
93+
# space = 0x00
94+
95+
mcHeader = MemoryConfigurationHeader(0xF8)
96+
self.assertEqual(mcHeader.customSpace, 0xF8)
97+
self.assertEqual(mcHeader.spaceIndex, MemorySpaceIndex.Custom)
98+
99+
mcHeader = MemoryConfigurationHeader(0xFF)
100+
self.assertIs(mcHeader.spaceIndex, MemorySpaceIndex.CDI)
101+
self.assertEqual(mcHeader.spaceIndex.value, 0x03)
102+
self.assertIsNone(mcHeader.customSpace)
103+
104+
mcHeader = MemoryConfigurationHeader(0xFD)
105+
self.assertIs(mcHeader.spaceIndex, MemorySpaceIndex.Configuration)
106+
self.assertEqual(mcHeader.spaceIndex.value, 0x01)
107+
self.assertIsNone(mcHeader.customSpace)

0 commit comments

Comments
 (0)