Skip to content

Commit c56854c

Browse files
committed
Watch remotely written CDIVar(s) by space and address, preserving type information.
1 parent d5b8232 commit c56854c

3 files changed

Lines changed: 227 additions & 21 deletions

File tree

examples/example_node_memory_implementation.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
the address and port). Defaults to a hard-coded test
1919
address and port.
2020
'''
21+
from collections import OrderedDict
2122
import os
2223
import socket
2324
import struct
@@ -28,6 +29,7 @@
2829

2930
# region same code as other examples
3031
from examples_settings import Settings
32+
from openlcb.cdivar import CDIVar
3133
from openlcb.convert import Convert
3234
from openlcb.localnode import LocalNode
3335
from openlcb.memoryspace import MemorySpace
@@ -208,8 +210,47 @@ def memoryReadFail(memo):
208210
# localNodeProcessor = LocalNodeProcessor(canLink, localNode)
209211
# canLink.registerMessageReceivedListener(localNodeProcessor.process)
210212
localNodeProcessor = localNode.localNodeProcessor
211-
localNode.setInt(0, 0, settings['port'], 2, False)
212-
localNode.setFloat(0, 2, settings['timeout'], 2)
213+
214+
# region simple local configuration
215+
# localNode.setInt(0, 0, settings['port'], 2, False)
216+
# localNode.setFloat(0, 2, settings['timeout'], 2)
217+
# endregion simple local configuration
218+
219+
220+
# region observable configuration
221+
222+
223+
def valueChangedRemotely(var: CDIVar):
224+
# f"REMOTE memory configuration: set value={repr(var.getSerializable())}"
225+
value = var.getInt() if (var.className == "int") else var.getFloat()
226+
data = var.getData()
227+
if data is None:
228+
data = bytearray([])
229+
print()
230+
print(
231+
f"[valueChangedRemotely] set {type(value).__name__}"
232+
f" value={repr(value)} ({Convert.toHex(data)})"
233+
f" at address {var.address} (tag={var.tag})")
234+
return
235+
236+
237+
vars = OrderedDict()
238+
vars['port'] = CDIVar("int", space=0, address=0, _size=2,
239+
_default=CDIVar.fromInt(12021, 2))
240+
port = settings['port']
241+
assert port is not None
242+
vars['port'].setInt(port)
243+
vars['timeout'] = CDIVar("float", space=0, address=2, _size=2,
244+
_default=CDIVar.fromFloat(0.5, 2))
245+
timeout = settings['timeout']
246+
assert timeout is not None
247+
vars['timeout'].setFloat(timeout)
248+
# assert vars['timeout'].getData() == bytearray([0x38, 0])
249+
for name, var in vars.items():
250+
var.tag = name # optional application-specific data
251+
memoryService.memory.registerWatchVar(var)
252+
memoryService.memory.registerWriteListener(valueChangedRemotely)
253+
# endregion observable configuration
213254

214255

215256
def displayOtherNodeIds(message: Message) :

openlcb/cdivar.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from openlcb import emit_cast, formatted_ex
1212
from openlcb.eventid import EventID
13+
from openlcb.memoryspace import MemorySpace
1314
from openlcb.openlcbaction import OpenLCBAction
1415

1516
logger = getLogger(__name__)
@@ -104,6 +105,9 @@ class CDIVar:
104105
.signed = True)
105106
_max (int): Maximum value (only for int/float className)
106107
_size (int): Size of int/float (not allowed for other className).
108+
tag (str): A key that can be used to access the var externally
109+
such as if in a dictionary, or any other data useful to
110+
the application.
107111
108112
Attributes:
109113
className (str): An OpenLCB CDI type. Must be a key in
@@ -131,13 +135,28 @@ class CDIVar:
131135
"""
132136
TYPED_KEYS = ['min', 'max', 'default']
133137

134-
def __init__(self, className, _min=None, _max=None,
135-
_size=None, _default=None, assert_range=False,
136-
_no_min=False, _no_max=False, _default_data=None,
137-
signed=None):
138+
def __init__(self, className,
139+
_min: Union['CDIVar', None] = None,
140+
_max: Union['CDIVar', None] = None,
141+
_size: Union[int, None] = None,
142+
_default: Union['CDIVar', None] = None,
143+
assert_range=False,
144+
_no_min=False, _no_max=False,
145+
_default_data: Union[bytearray, bytes, None] = None,
146+
signed: Union[bool, None] = None,
147+
space: Union[MemorySpace, int, None] = None,
148+
address: Union[int, None] = None,
149+
tag: Union[str, None] = None):
138150
self.data = None # type: bytes|None
139151
self.min = _min # type: CDIVar|None
140152
self.max = _max # type: CDIVar|None
153+
self.tag = tag # type: str|None
154+
if space is not None:
155+
assert isinstance(space, (MemorySpace, int))
156+
self.space = space.value if isinstance(space, MemorySpace) else space
157+
if address is not None:
158+
assert isinstance(address, int)
159+
self.address = address
141160

142161
assert isinstance(className, str), \
143162
f"Expected {CLASSNAME_TYPES.keys()} got {emit_cast(className)}"
@@ -196,7 +215,7 @@ def __init__(self, className, _min=None, _max=None,
196215
logger.error(error)
197216
elif (className in NUM_TYPES) and not _no_min:
198217
# self.min = CDIVar(className, _size=_size,
199-
# _no_min=True, _no_max=True) # prevent inf recurs
218+
# _no_min=True, _no_max=True) # prevent inf rec
200219
# Set minimum based on size,
201220
# as per Configuration Description Information Standard.
202221
assert _size is not None
@@ -267,9 +286,17 @@ def __init__(self, className, _min=None, _max=None,
267286
(f"Expected size in {sizes}"
268287
f" for {self.className} but got {self.size}")
269288
self.floatFormat = None # type: str|None
270-
self.address = None # type: int|None
271289
self.element = None # type: Any|None
272-
self.space = None # type: int|None
290+
291+
def copy(self, assert_range=False) -> 'CDIVar':
292+
default = None
293+
if self.default is not None:
294+
default = self.default.copy()
295+
return CDIVar(self.className, _min=self.min, _max=self.max,
296+
_size=self.size, _default=default,
297+
assert_range=assert_range, _no_min=self._no_min,
298+
_no_max=self._no_max, signed=self.signed,
299+
space=self.space, address=self.address)
273300

274301
@staticmethod
275302
def cmp_float(left: 'CDIVar', right: Union['CDIVar', float]) -> CompareOp:
@@ -606,15 +633,22 @@ def getDict(self, add_name=True):
606633
return result
607634

608635
def setInt(self, value: int):
636+
if self.className != "int":
637+
logger.warning(
638+
f"setInt called on {self.className} CDIVar (may be wrong)!")
609639
self.data = self.intToData(value)
610640

611641
def floatToData(self, value: float) -> bytes:
612-
assert self.className == "float", \
613-
f"floatToData attempted on non-float: {self.className}"
642+
if self.className != "float":
643+
logger.warning(
644+
f"floatToData on {self.className} CDIVar (may be wrong)!")
614645
assert isinstance(value, float)
615646
return struct.pack(self.packFormat(), value)
616647

617648
def setFloat(self, value: float):
649+
if self.className != "float":
650+
logger.warning(
651+
f"setFloat called on {self.className} CDIVar (may be wrong)!")
618652
self.data = self.floatToData(value)
619653

620654
def stringToData(self, value: str) -> bytes:

openlcb/memorymanager.py

Lines changed: 141 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from logging import getLogger
22
import struct
3-
from typing import Union
3+
from typing import Callable, Union
44

55
from openlcb.cdivar import SUBTYPE_FORMATS, CDIVar
66
from openlcb.memoryspace import MemorySpace
@@ -9,6 +9,8 @@
99

1010

1111
class Segment:
12+
"""A memory segment that is contiguous or behaves as such.
13+
"""
1214
def __init__(self, size=0, readOnly=False):
1315
assert isinstance(size, int)
1416
assert size >= 0
@@ -78,7 +80,7 @@ def extend(self, data):
7880
self._data += data
7981

8082
def setSlice(self, address: int, data: Union[bytearray, bytes],
81-
size: Union[int, None] = None, force=True):
83+
size: Union[int, None] = None, force=True):
8284
assert isinstance(data, (bytearray, bytes))
8385
assert isinstance(address, int)
8486
assert address >= 0
@@ -109,16 +111,37 @@ def size(self) -> int:
109111

110112

111113
class MemoryManager:
114+
"""A collection of memory segments.
115+
Attributes:
116+
watchVars (Dict[int, Dict[int, CDIVar]]): Variables that will be
117+
used as the argument to callbacks, as well as set when
118+
memory is set, at the address corresponding to the 2nd
119+
tier's integer key. The 1st tier of the dict is keyed by a
120+
space int.
121+
- If not present, and setSlice is used (without the optional
122+
callbackVar) no registered callbacks will be called (Not
123+
enough information).
124+
125+
"""
126+
112127
def __init__(self):
113128
self._segments = {} # type: dict[int, Segment]
129+
self._writeListeners = []
130+
self.watchVars = {} # type: dict[int, dict[int, CDIVar]]
114131

115132
def set(self, var: CDIVar):
116-
assert isinstance(var, CDIVar)
133+
assert issubclass(type(var), CDIVar)
117134
assert var.space is not None
118-
assert var.address
135+
assert var.address is not None
119136
data = var.getData()
120137
assert data is not None
121-
self.setSlice(var.space, var.address, data, size=var.size)
138+
watchVar = self.getWatchVar(var.space, var.address)
139+
if watchVar is not None:
140+
var = watchVar
141+
assert var.space is not None
142+
assert var.address is not None
143+
self.setSlice(var.space, var.address, data, size=var.size,
144+
callbackVar=var)
122145

123146
def getFirst(self, space: Union[MemorySpace, int]):
124147
"""Get first address"""
@@ -196,7 +219,7 @@ def get(self, var: CDIVar) -> CDIVar:
196219
Returns:
197220
CDIVar: Same var instance (returned by reference) modified.
198221
"""
199-
assert isinstance(var, CDIVar)
222+
assert issubclass(type(var), CDIVar)
200223
assert var.space is not None
201224
assert var.address
202225
assert var.size is not None
@@ -206,8 +229,13 @@ def get(self, var: CDIVar) -> CDIVar:
206229
return var
207230

208231
def setSlice(self, space: Union[MemorySpace, int], address: int,
209-
data: Union[bytes, bytearray], size=None):
210-
"""Set address in virtual memory space to data"""
232+
data: Union[bytes, bytearray], size=None,
233+
callbackVar: Union[CDIVar, None] = None) -> Segment:
234+
"""Set address in virtual memory space to data.
235+
fireWriteListeners can only be called if
236+
callbackVar is known, otherwise type information
237+
is not available at this level.
238+
"""
211239
assert isinstance(data, (bytearray, bytes))
212240
if isinstance(space, MemorySpace):
213241
space = space.value
@@ -223,6 +251,18 @@ def setSlice(self, space: Union[MemorySpace, int], address: int,
223251
segment = Segment()
224252
self._segments[space] = segment
225253
segment.setSlice(address, data, size=size)
254+
if callbackVar is None:
255+
callbackVar = self.getWatchVar(space, address)
256+
if callbackVar is not None:
257+
print(f"[setSlice] getWatchVar {callbackVar.className}")
258+
else:
259+
print(f"[setSlice] getWatchVar (None)")
260+
else:
261+
print(f"[setSlice] callbackVar {callbackVar.className}")
262+
if callbackVar is not None:
263+
callbackVar.setData(data)
264+
self.fireWriteListeners(callbackVar)
265+
return segment
226266

227267
def getSlice(self, space: Union[MemorySpace, int], address: int,
228268
size: int, force=False) -> bytearray:
@@ -255,7 +295,14 @@ def setInt(self, space: Union[MemorySpace, int], address: int,
255295
data = struct.pack(dataFormat, value)
256296
assert len(data) == size, \
257297
f"Expected {size} byte(s) for {typeStr}, got {len(data)}"
258-
return self.setSlice(space, address, data)
298+
var = self.getWatchVar(space, address)
299+
if var is None:
300+
var = CDIVar("int", _size=size, _no_min=True, _no_max=True,
301+
space=space, address=address)
302+
result = self.setSlice(space, address, data, callbackVar=var)
303+
if self._writeListeners:
304+
self.fireWriteListeners(var)
305+
return result
259306

260307
def getInt(self, space: Union[MemorySpace, int], address: int,
261308
size: int, signed: bool) -> int:
@@ -289,7 +336,14 @@ def setFloat(self, space: Union[MemorySpace, int], address: int,
289336
data = struct.pack(dataFormat, value)
290337
assert len(data) == size, \
291338
f"Expected {size} byte(s) for {typeStr}, got {len(data)}"
292-
return self.setSlice(space, address, data)
339+
var = self.getWatchVar(space, address)
340+
if var is None:
341+
var = CDIVar("float", _size=size, _no_min=True, _no_max=True,
342+
space=space, address=address)
343+
result = self.setSlice(space, address, data, callbackVar=var)
344+
if self._writeListeners:
345+
self.fireWriteListeners(var)
346+
return result
293347

294348
def getFloat(self, space: Union[MemorySpace, int], address: int,
295349
size: int) -> float:
@@ -306,3 +360,80 @@ def getFloat(self, space: Union[MemorySpace, int], address: int,
306360
assert len(values) == 1, f"Expected 1 {typeStr}, got {len(values)}"
307361
assert isinstance(values[0], float)
308362
return values[0]
363+
364+
def registerWriteListener(self, callback: Callable[[CDIVar], None]):
365+
"""Register a function to call when a value is written.
366+
NOTE: You must also call registerWatchVar or low-level
367+
(setSlice) calls will not trigger a callback due to not enough
368+
information (callback takes a CDIVar).
369+
"""
370+
self._writeListeners.append(callback)
371+
372+
def registerWatchVar(self, var: CDIVar):
373+
"""Register a CDIVar to track writes.
374+
375+
Arguments:
376+
var (CDIVar): Must have var.space
377+
and var.address in order to be tracked. This var's value
378+
will be edited remotely, and will allow write listeners
379+
to fire even when setting memory of a non-number or
380+
unspecified type (using setSlice).
381+
Raises:
382+
AssertionError: space or address is None.
383+
"""
384+
# a.k.a. setWatchVar
385+
assert var.space is not None, \
386+
'cdivar.space is required in order to listen for change'
387+
if isinstance(var.space, MemorySpace):
388+
var.space = var.space.value
389+
assert isinstance(var.space, int)
390+
assert var.address is not None, \
391+
'cdivar.address is required in order to listen for change'
392+
assert isinstance(var.address, int)
393+
if var.space not in self.watchVars:
394+
self.watchVars[var.space] = {}
395+
if var.address in self.watchVars[var.space]:
396+
raise KeyError(
397+
f"Address {var.address} of space {var.space}"
398+
" is already registered.")
399+
self.watchVars[var.space][var.address] = var
400+
401+
def getWatchVar(self, space: Union[MemorySpace, int], address: int,
402+
default: Union[CDIVar, None] = None):
403+
assert space is not None
404+
assert address is not None
405+
if isinstance(space, MemorySpace):
406+
space = space.value
407+
assert isinstance(space, int)
408+
assert isinstance(address, int)
409+
if default is not None:
410+
assert issubclass(type(default), CDIVar)
411+
spaceVars = self.watchVars.get(space)
412+
if spaceVars is None:
413+
return None
414+
var = spaceVars.get(address, default)
415+
if var is not None:
416+
# Fix space & address so fireWriteListeners works correctly
417+
if var.space is None:
418+
logger.warning(
419+
f"Setting var.space={space} using its location")
420+
var.space = space
421+
elif var.space != space:
422+
logger.warning(
423+
f"Setting incorrect var.space {var.space}"
424+
f" to {space} using its location")
425+
var.space = space
426+
if var.address is None:
427+
logger.warning(
428+
f"Setting var.address={address} using its location")
429+
var.address = address
430+
elif var.address != address:
431+
logger.warning(
432+
f"Setting incorrect var.address {var.address}"
433+
f" to {address} using its location")
434+
var.address = address
435+
return var
436+
437+
def fireWriteListeners(self, var: CDIVar):
438+
for writeListener in self._writeListeners:
439+
writeListener(var)

0 commit comments

Comments
 (0)