Skip to content

Commit f42443e

Browse files
committed
Fix local memory read (Encode and decode space index correctly; Pad with zeroes if last chunk less than 64 bytes). Add a related test case. Add increment option to generate_node_id.
1 parent 525e9f8 commit f42443e

13 files changed

Lines changed: 379 additions & 34 deletions

openlcb/conventions.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def get_local_ip() -> Optional[str]:
146146

147147
previous_three_octets = None
148148
initial_three_octets = None
149+
used_ids = set()
149150

150151

151152
def increment_octets(octets: bytearray):
@@ -241,6 +242,8 @@ def generate_node_id_str(id_range_prefix: str, increment: bool = False) -> str:
241242
python-openlcb (or as otherwise assigned by OpenLCB Group
242243
which reserves 05.* range). See
243244
<https://registry.openlcb.org/uniqueidranges>.
245+
increment (bool): Increment such as more multiple
246+
local IDs (virtual node(s) aside from local Node).
244247
Returns:
245248
str: Full 48-bit node ID in dotted hex string notation (Example:
246249
'05.01.01.4A.B7.19') that is unique (very likely...).
@@ -260,5 +263,10 @@ def generate_node_id_str(id_range_prefix: str, increment: bool = False) -> str:
260263
" (preferably less to increase likelihood of uniqueness). Got {}"
261264
.format(id_range_prefix))
262265
uniqueCount = 6 - len(prefixParts)
263-
return ".".join(prefixParts+lastParts[-uniqueCount:])
266+
id_str = ".".join(prefixParts+lastParts[-uniqueCount:])
264267
# ^ negative to keep last uniqueCount pairs
268+
if used_ids in used_ids:
269+
logger.warning(f"{id_str} was already used. Set increment=True"
270+
" if there is more than one local node!")
271+
used_ids.add(id_str)
272+
return id_str

openlcb/linklayer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ class State(Enum):
4848
# (enforced using type(self).__name__ != "LinkLayer" checks in methods)
4949

5050
def __init__(self, physicalLayer: PhysicalLayer, localNodeID):
51-
assert isinstance(physicalLayer, PhysicalLayer) # allows any subclass
51+
assert isinstance(physicalLayer, PhysicalLayer), \
52+
f"Expected PhysicalLayer/subclass, got a(n) {type(physicalLayer)}"
53+
# ^ allows any subclass
5254
# subclass should check type of localNodeID technically
5355
self.localNodeID = localNodeID
5456
self._messageReceivedListeners = []

openlcb/memoryconfigurationheader.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,11 @@ def fromMC2ndByte(cls, datagramByte1: int, space: Union[int, None] = None) -> 'M
5151
'custom space requires datagramByte1 with last 2 bits 00'
5252
else:
5353
# space is None
54-
assert datagramByte1 & 0x03 != 0, \
55-
'a standard space index must be in last 2 bits datagramByte1'
54+
# assert datagramByte1 & 0x03 != 0, \
55+
# ("standard space index req. in low 2 bits of datagramByte1"
56+
# f" but got {hex(datagramByte1)}")
57+
# ^ commented to allow indeterminate state,
58+
# so caller can set space later from the later space byte
5659
space = -1
5760
# NOTE: Third option is that space isn't known yet
5861
# (must be set later, if spaceIsCustom())

openlcb/memorymanager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ def setData(self, address: int, data: Union[bytearray, bytes],
104104
else:
105105
self._data[address:end] = data
106106

107+
def size(self) -> int:
108+
return len(self._data)
109+
107110

108111
class MemoryManager:
109112
def __init__(self):

openlcb/memoryreadjob.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ def echoS(message: str):
6363

6464
if isinstance(space, MemorySpace):
6565
space = space.value
66-
echoS("Requesting memory read. Please wait...")
66+
echoS("")
67+
echoS(f"Requesting memory read (space={space}). Please wait...")
6768
# read 64 bytes from the CDI space starting at address zero
6869
self.memMemo = MemoryReadMemo(farNodeID, 64, space, 0,
6970
self.memoryReadFail,
@@ -85,9 +86,9 @@ def memoryReadSuccess(self, memo):
8586
if len(memo.data) == 64 and 0 not in memo.data:
8687
# save content
8788
self.resultingCDI += memo.data
88-
logger.debug(
89+
logger.info(
8990
f"[{memo.address}] successful read"
90-
f" {Convert.arrayToString(memo.data, len(memo.data))}"
91+
f" `{Convert.arrayToString(memo.data, len(memo.data))}`"
9192
"; next = address + 64")
9293
# update the address
9394
memo.address = memo.address+64
@@ -120,8 +121,11 @@ def memoryReadSuccess(self, memo):
120121
memo.done = True
121122
# done
122123

123-
def memoryReadFail(self, memo):
124-
print("memory read failed: {}".format(memo.data))
124+
def memoryReadFail(self, memo: MemoryReadMemo):
125+
assert isinstance(memo, MemoryReadMemo)
126+
print(f"memory read failed: id={memo.nodeID}"
127+
f" data={memo.data} space={memo.space} address={memo.address}"
128+
f" error={memo.error} code={memo.errorCode}")
125129
self.failed = True
126130

127131
def processXML(self, content: str) :

openlcb/memoryservice.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from openlcb.convert import Convert
4444
# from openlcb.localnode import LocalNode # circular import
4545
from openlcb.memoryconfigurationheader import MemoryConfigurationHeader
46+
from openlcb.memoryspace import MemorySpace
4647
from openlcb.memoryspaceindex import MemorySpaceIndex
4748
from openlcb.memorymanager import MemoryManager
4849
from openlcb.nodeid import NodeID
@@ -328,13 +329,17 @@ def parseReplyDatagram(memo: Union[MemoryReadMemo, MemoryWriteMemo],
328329
)
329330
offset = 6
330331
error = None
331-
assert mcHeader.spaceIndex is not MemorySpaceIndex.Uninitialized
332-
if mcHeader.spaceIndex is MemorySpaceIndex.Custom:
332+
if mcHeader.spaceIndex in (MemorySpaceIndex.Custom,
333+
MemorySpaceIndex.Uninitialized):
333334
# mcHeader.customSpace = memo.space
334335
mcHeader.customSpace = dmemo.data[6]
335336
offset = 7
337+
else:
338+
assert mcHeader.customSpace is None, \
339+
"fromMC2ndByte should not set customSpace in this case"
336340
memo.error = None
337341
memo.errorCode = None
342+
print(f"reply datagram: {mcHeader.spaceIndex} customSpace={mcHeader.customSpace}")
338343
if (dmemo.data[1] & 0x08 == 0):
339344
# ok reply
340345
return
@@ -625,42 +630,55 @@ def datagramReceivedListener(self, dmemo: DatagramReadMemo) -> bool:
625630
self.spaceLengthCallback(address)
626631
self.spaceLengthCallback = None
627632
elif mcOp is MCOp.Read_Command:
628-
# assert dmemo.data[1] in TWO_BIT_PARAMS[MCOp.Read_Command.value], \
633+
# assert dmemo.data[1] in TWO_BIT_PARAMS[MCOp.Read_Command.value],\
629634
# "self-test failed (bad constant(s))"
630635
mcHeader = MemoryConfigurationHeader.fromMC2ndByte(dmemo.data[1])
631636
addressBytes = dmemo.data[2:6]
632637
address = struct.unpack(">I", addressBytes)[0]
633638
# ^ [0] since always returns list even when reading 1 value.
634639
# ^ capital assumes unsigned, "I" assumes 32-bit (4 bytes)
635-
space = dmemo.data[1] & 0b00000011
640+
spaceIndex = dmemo.data[1] & 0b00000011
636641
offset = 0
642+
space = None
637643
if mcHeader.spaceIsCustom():
638-
assert space == 0
644+
assert spaceIndex == 0
639645
space = dmemo.data[6]
640646
offset = 1
641647
size = dmemo.data[6+offset] # requested read count
642648
datagramBytes = bytearray([0x20, MCOp.Read_Reply.value])
643649
# ^ byte1 (2nd) changed to error below if applicable
644-
assert isinstance(space, int), \
645-
(f"Logic missing, space should be number here,"
646-
f" got {emit_cast(space)}")
650+
assert isinstance(spaceIndex, int), \
651+
(f"Logic missing, spaceIndex should be number here,"
652+
f" got {emit_cast(spaceIndex)}")
647653
assert isinstance(address, int)
648654
datagramBytes += addressBytes
649-
assert space is not None, \
650-
f"space not computed from datagram: {dmemo.data}"
651655
if mcHeader.spaceIsCustom():
656+
assert space is not None
652657
datagramBytes.append(space)
653658
assert len(datagramBytes) == 7, "space goes in index [6]"
654659
else:
660+
space = MemorySpace.fromIndex(MemorySpaceIndex(spaceIndex))
661+
assert space is not None
662+
space = space.value
655663
spaceIndex = (space & 0b00000011)
656-
assert spaceIndex == space
657664
assert space is not None
658665
datagramBytes[1] = \
659666
datagramBytes[1] | spaceIndex
660667
assert len(datagramBytes) == 6, "should not have space in [6]"
668+
assert space is not None, \
669+
f"space not computed from datagram: {dmemo.data}"
661670
payload = None
662671
try:
663-
payload = self.memory.getSlice(space, address, size)
672+
segment = self.memory.getStorage(space)
673+
if segment is None:
674+
raise KeyError(f"space {space} is not valid")
675+
if address >= segment.size():
676+
raise IndexError(
677+
f"address {address} past end of {hex(space)}")
678+
payload = self.memory.getSlice(space, address, size,
679+
force=True)
680+
# ^ force=True because reading past end is normal
681+
# (pad with zeroes to indicate end)
664682
except (IndexError, KeyError) as ex:
665683
# address out of range (See Segment's getSlice)
666684
datagramBytes[1] = MCOp.Read_Reply_Failure.value
@@ -698,6 +716,10 @@ def datagramReceivedListener(self, dmemo: DatagramReadMemo) -> bool:
698716
dmemo.srcID,
699717
datagramBytes
700718
)
719+
# hexStrings = []
720+
# for b in datagramBytes:
721+
# hexStrings.append(hex(b))
722+
# print(f"Sending read reply: {hexStrings}")
701723
self.service.sendDatagram(requestedMemoryMemo)
702724
else:
703725
logger.error("Did not expect reply of type 0x{:02X}"

openlcb/nodeid.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
from logging import getLogger
2+
13
from openlcb import emit_cast
24
from openlcb.conventions import generate_node_id_str
35

6+
logger = getLogger(__name__)
7+
48

59
class NodeID:
610
"""A 6-byte (48-bit) Node ID.
@@ -88,7 +92,7 @@ def __hash__(self):
8892
return hash(self.value)
8993

9094

91-
def generate_node_id(id_range_prefix):
95+
def generate_node_id(id_range_prefix, increment=False):
9296
"""Generate a unique NodeID for the session to ensure each
9397
instance (even of python-openlcb on same device) or
9498
locally-generated virtual node is unique.
@@ -99,7 +103,9 @@ def generate_node_id(id_range_prefix):
99103
python-openlcb (or as otherwise assigned by OpenLCB Group
100104
which reserves 05.* range). See
101105
<https://registry.openlcb.org/uniqueidranges>.
106+
increment (bool): Increment such as more multiple
107+
local IDs (virtual node(s) aside from local Node).
102108
Returns:
103109
NodeID: A NodeID that is unique (very likely...).
104110
"""
105-
return NodeID(generate_node_id_str(id_range_prefix))
111+
return NodeID(generate_node_id_str(id_range_prefix, increment=increment))

openlcb/openlcbnetwork.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,8 @@ def _fireStatus(self, status,
368368
print("OpenLCBNetwork callback_msg({})".format(repr(status)))
369369
callback(CDIMemo(status=status))
370370
else:
371-
logger.warning("No callback, but set status: {}".format(status))
371+
logger.warning(
372+
f"[OpenLCBNetwork] No callback, but set status: {status}")
372373

373374
def _memoryReadSuccess(self, memo: MemoryReadMemo, force_end=False):
374375
"""Handle a successful read

openlcb/physicallayer.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,18 @@ def encodeFrameAsData(self, frame) -> Union[bytearray, bytes]:
226226

227227
def physicalLayerUp(self):
228228
"""abstract method"""
229-
raise NotImplementedError("Each subclass must implement this.")
229+
raise NotImplementedError(
230+
f"{type(self).__name__} subclass must implement physicalLayerUp.")
230231

231232
def physicalLayerRestart(self):
232233
"""abstract method"""
233-
raise NotImplementedError("Each subclass must implement this.")
234+
raise NotImplementedError(
235+
f"{type(self).__name__} subclass must implement physicalLayerRestart.")
234236

235237
def physicalLayerDown(self):
236238
"""abstract method"""
237-
raise NotImplementedError("Each subclass must implement this.")
239+
raise NotImplementedError(
240+
f"{type(self).__name__} subclass must implement physicalLayerDown.")
238241

239242
def handleData(self, data: Union[bytes, bytearray]) -> int:
240243
"""abstract method (accept data, return # of frames created)"""

openlcb/portinterface.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ def setListeners(self, onReadyToSend, onReadyToReceive):
6969

7070
def _settimeout(self, seconds):
7171
"""Abstract method. Return: implementation-specific or None."""
72-
raise NotImplementedError("Subclass must implement this.")
72+
raise NotImplementedError(
73+
f"{type(self).__name__} subclass must implement _settimeout.")
7374

7475
def settimeout(self, seconds):
7576
return self._settimeout(seconds)
@@ -79,7 +80,8 @@ def _connect(self, host: Any, port: Any, device: Any = None):
7980
See connect for details.
8081
raise exception on failure to prevent self._open = True.
8182
"""
82-
raise NotImplementedError("Subclass must implement this.")
83+
raise NotImplementedError(
84+
f"{type(self).__name__} subclass must implement _connect.")
8385

8486
def connect(self, host, port, device=None):
8587
"""Connect to a port.
@@ -109,7 +111,8 @@ def connectLocal(self, port):
109111

110112
def _send(self, data: Union[bytes, bytearray]) -> None:
111113
"""Abstract method. Return: implementation-specific or None"""
112-
raise NotImplementedError("Subclass must implement this.")
114+
raise NotImplementedError(
115+
f"{type(self).__name__} subclass must implement _send.")
113116

114117
def send(self, data: Union[bytes, bytearray]) -> None:
115118
"""
@@ -132,7 +135,8 @@ def send(self, data: Union[bytes, bytearray]) -> None:
132135

133136
def _receive(self) -> Union[bytearray, bytes, None]:
134137
"""Abstract method. Return (bytes): data"""
135-
raise NotImplementedError("Subclass must implement this.")
138+
raise NotImplementedError(
139+
f"{type(self).__name__} subclass must implement _receive.")
136140

137141
def receive(self) -> Union[bytearray, bytes, None]:
138142
self._setBusy("receive")
@@ -147,7 +151,8 @@ def receive(self) -> Union[bytearray, bytes, None]:
147151

148152
def _close(self) -> None:
149153
"""Abstract method. Return: implementation-specific or None"""
150-
raise NotImplementedError("Subclass must implement this.")
154+
raise NotImplementedError(
155+
f"{type(self).__name__} subclass must implement _close.")
151156

152157
def setOpen(self, is_open):
153158
if self._open != is_open:

0 commit comments

Comments
 (0)