Skip to content

Commit a650a38

Browse files
committed
Add "Read" button for configuration variables (and WIP writing code, hidden until next commit).
1 parent a1ebe25 commit a650a38

5 files changed

Lines changed: 171 additions & 18 deletions

File tree

examples/examples_gui.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -453,14 +453,15 @@ def _gui(self, parent):
453453
command=self.cdiConnectClicked,
454454
)
455455
self.cdi_connect_button.grid(row=self.cdi_row, column=0)
456+
self.cdi_row += 1
456457

457458
self.cdi_refresh_button = ttk.Button(
458459
self.cdi_tab,
459460
text="Refresh",
460461
command=self.cdiRefreshClicked,
461462
state=tk.DISABLED, # enabled on connect success callback
462463
)
463-
self.cdi_refresh_button.grid(row=self.cdi_row, column=1)
464+
self.cdi_refresh_button.grid(row=self.cdi_row, column=0)
464465
self.cdi_row += 1
465466

466467
self.cdiSettingFrame = ttk.Frame(self.cdi_tab)
@@ -502,7 +503,7 @@ def _gui(self, parent):
502503

503504
def setupNetwork(self):
504505
self.network = OpenLCBNetwork(self.getValue('localNodeID'))
505-
self.cdi_form = CDIForm(self.network.canLink, self.cdi_tab)
506+
self.cdi_form = CDIForm(self.cdi_tab, self.network.canLink, self)
506507
self.cdi_form.setSettingsContainer(self.cdiSettingFrame)
507508
self.cdi_form.setStatusCallback(self.setStatus)
508509
# ^ formerly OpenLCBNetwork() subclass
@@ -609,7 +610,6 @@ def cdiConnectClicked(self):
609610
# This thread may end quickly after connection since
610611
# start_receiving starts a thread.
611612
self.cdi_connect_button.configure(state=tk.DISABLED)
612-
self.cdi_connect_button.configure(state=tk.DISABLED)
613613

614614
def cdiRefreshClicked(self):
615615
self.cdi_connect_button.configure(state=tk.DISABLED)

examples/tkexamples/cdiform.py

Lines changed: 145 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
1010
Contributors: Poikilos
1111
"""
12+
from functools import partial
1213
import logging
1314
import os
1415
import sys
@@ -23,6 +24,9 @@
2324
from xml.etree import ElementTree as ET
2425

2526
from openlcb.cdivar import CLASSNAME_TYPES, CDIVar
27+
from openlcb.memoryservice import MemoryReadMemo
28+
from openlcb.nodeid import NodeID
29+
from openlcb.openlcbnetwork import OpenLCBNetwork
2630

2731

2832
if __name__ == "__main__":
@@ -67,22 +71,30 @@ class CDIForm(ttk.Frame, XMLDataProcessor):
6771
Args:
6872
parent (TkWidget): Typically a ttk.Frame or tk.Frame with "root"
6973
attribute set.
74+
linkLayer (LinkLayer): Typically a CanLink instance.
7075
"""
7176
def __init__(self, *args, **kwargs):
72-
assert isinstance(args[0], LinkLayer), \
77+
assert issubclass(type(args[1]), LinkLayer), \
7378
"Expected LinkLayer/subclass got {}".format(type(args[0]).__name__)
74-
linkLayer = args[0]
75-
args = args[1:] # remove first argument (only for GUI)
79+
linkLayer = args[1]
80+
assert issubclass(type(args[0]), tk.Widget)
7681
XMLDataProcessor.__init__(self, linkLayer, MemorySpace.CDI)
77-
ttk.Frame.__init__(self, *args, **kwargs)
82+
ttk.Frame.__init__(self, *args[:1], **kwargs)
7883
self._top_widgets = []
7984
if len(args) < 1:
8085
raise ValueError("at least one argument (parent) is required")
8186
self.parent = args[0]
82-
self.root = args[0]
87+
self.mainform = args[2]
88+
self.root = args[2]
89+
assert hasattr(self.mainform, 'network'), \
90+
"mainform must have 'network' OpenLCBNetwork"
91+
assert hasattr(self.mainform, 'settings'), \
92+
"mainform must have 'settings' dictionary with at least 'farNodeID'"
8393
self._status_callback = None
8494
if hasattr(self.parent, 'root'):
8595
self.root = self.parent.root
96+
elif hasattr(self.mainform, 'root'):
97+
self.root = self.mainform.root
8698
self._container = self # where to put visible widgets
8799
self._treeview = None # type: ttk.Treeview|None
88100
self._treeMemos = {} # type: Dict[str, CDIMemo]
@@ -178,7 +190,7 @@ def onTreeSelect(self, event: tk.Event):
178190
cm = self._treeMemos[iid]
179191
# print(f"type(item)={type(item)}")
180192
# raise NotImplementedError(item)
181-
# print(f"cm={cm}")
193+
print(f"cm={cm}")
182194
self.clearSettingWidgets()
183195
if cm.tag not in CLASSNAME_TYPES:
184196
# Non-value (such as segment or group)
@@ -199,7 +211,7 @@ def onTreeSelect(self, event: tk.Event):
199211
tkvar = None
200212
v_widget = None
201213
mapToValue = cm.valueMap()
202-
self.mapToValue = mapToValue
214+
# self.mapToValue = mapToValue
203215
if mapToValue:
204216
tkvar = tk.StringVar(self.root)
205217
v_widget = ttk.Combobox(self.cdiSettingFrame,
@@ -215,22 +227,58 @@ def onTreeSelect(self, event: tk.Event):
215227
raise TypeError("Device should not specify max for {}"
216228
.format(cdivar.className))
217229
v_widget = ttk.LabeledScale(self.cdiSettingFrame,
218-
variable=tkvar)
230+
variable=tkvar,
231+
from_=cdivar.min,
232+
to=cdivar.max)
219233
# ^ widget.scale is ttk.Scale, widget.label is ttk.Label
220234
# ^ a.k.a. Slider (if not using Tk)
221235
v_widget.scale.cdivar = cdivar
222236
v_widget.scale.tip = nameLabel.tip
237+
238+
def update_label_width(*args):
239+
# Fix label being too small to show value:
240+
v_widget.label.configure(width=len(str(int(tkvar.get()))) + 2) # noqa: E501
241+
tkvar.trace_add('write', update_label_width)
223242
else:
224243
tkvar = tk.StringVar(self.root)
225244
v_widget = ttk.Entry(self.cdiSettingFrame, textvariable=tkvar)
226245
v_widget.grid(column=1, row=self.cdiSettingRow)
227246
self.cdiSettingRow += 1
228247
self.cdiSettingWidgets.append(v_widget)
229248
if cdivar.default is not None:
230-
tkvar.set(cdivar.default)
249+
defaultStr = cdivar.default
250+
if mapToValue:
251+
mapToStr = cm.keyMap()
252+
assert mapToStr
253+
defaultStr = mapToStr[str(cdivar.default.value())]
254+
tkvar.set(defaultStr)
231255
v_widget.cdivar = cdivar
232256
v_widget.tip = nameLabel.tip
257+
try:
258+
v_widget.configure(state='readonly') # readonly until refresh
259+
except tk.TclError:
260+
pass # N/A such as for Scale
261+
# so that previous value is known.
262+
263+
# Write and Read buttons
264+
writeButton = ttk.Button(
265+
self.cdiSettingFrame,
266+
text="Write",
267+
command=partial(self.onWriteValueClicked, v_widget, tkvar,
268+
cdivar, mapToValue=mapToValue),
269+
)
270+
# writeButton.grid(column=0, row=self.cdiSettingRow)
271+
272+
readButton = ttk.Button(
273+
self.cdiSettingFrame,
274+
text="Read",
275+
command=partial(self.onReadValueClicked, v_widget, tkvar,
276+
cdivar, mapToValue=mapToValue),
277+
)
278+
readButton.grid(column=1, row=self.cdiSettingRow)
279+
self.cdiSettingRow += 1
233280

281+
# Show the address
234282
address_str = ""
235283
if address_str is not None:
236284
address_str = str(cm.address)
@@ -241,9 +289,96 @@ def onTreeSelect(self, event: tk.Event):
241289
self.cdiSettingWidgets.append(a_widget)
242290
self.cdiSettingWidgets.append(av_widget)
243291
self.cdiSettingRow += 1
244-
245292
break
246293

294+
def onReadValueClicked(self, v_widget: tk.Widget,
295+
tkvar: Union[tk.StringVar, tk.IntVar, tk.DoubleVar],
296+
cdivar: CDIVar, mapToValue=None):
297+
print("read:"
298+
f" space={cdivar.space}={hex(cdivar.space)}"
299+
f" address={cdivar.address}={hex(cdivar.address)}"
300+
f" size={cdivar.size}={hex(cdivar.size)}")
301+
# read 64 bytes from the CDI space starting at address zero
302+
assert hasattr(self.mainform, 'settings'), \
303+
"mainform must have 'settings' dictionary"
304+
assert 'farNodeID' in self.mainform.settings, \
305+
"mainform 'settings' dictionary is missing 'farNodeID'"
306+
farNodeIDStr = self.mainform.settings['farNodeID']
307+
308+
memMemo = MemoryReadMemo(NodeID(farNodeIDStr),
309+
cdivar.size, cdivar.space,
310+
cdivar.address, self.memoryReadFail,
311+
self.memoryReadSuccess)
312+
memMemo.widget = v_widget
313+
memMemo.tkvar = tkvar
314+
memMemo.cdivar = cdivar
315+
memMemo.mapToValue = mapToValue
316+
network = self.mainform.network # type: OpenLCBNetwork
317+
network._memoryService.requestMemoryRead(memMemo)
318+
319+
def memoryReadFail(self, memo: MemoryReadMemo):
320+
self.setStatus("Memory read...error.")
321+
322+
def memoryReadSuccess(self, memo: MemoryReadMemo):
323+
self.setStatus("Memory read...success.")
324+
widget = None # type: tk.Widget | None
325+
tkvar = None # type: tk.StringVar | tk.IntVar | tk.DoubleVar | None
326+
cdivar = None # type: CDIVar | None
327+
if hasattr(memo, 'widget'):
328+
widget = memo.widget
329+
try:
330+
# widget['state'] = tk.NORMAL
331+
widget.configure(state=tk.NORMAL)
332+
except tk.TclError:
333+
pass # N/A (such as Scale)
334+
value = None
335+
mapToValue = None
336+
found = None
337+
if hasattr(memo, 'cdivar'):
338+
cdivar = memo.cdivar
339+
assert cdivar is not None
340+
cdivar.setData(memo.data)
341+
value = cdivar.value()
342+
if (value is not None) and hasattr(memo, 'mapToValue'):
343+
mapToValue = memo.mapToValue
344+
if mapToValue:
345+
for k, v in mapToValue.items():
346+
# if str(v.value()) == str(value):
347+
if v.value() == value:
348+
value = k # key is the user-facing string
349+
found = value
350+
break
351+
if found is None:
352+
logger.warning(
353+
f"Found no matching value for {repr(value)} in {mapToValue}")
354+
else:
355+
logger.warning(
356+
f"Found matching value {repr(value)} for {found}")
357+
else:
358+
logger.debug(
359+
f"There is no value map. Using {repr(value)} directly")
360+
else:
361+
logger.debug(
362+
f"There is no value map. Using {repr(value)} directly")
363+
else:
364+
logger.warning("There is no cdivar.")
365+
if hasattr(memo, 'tkvar'):
366+
tkvar = memo.tkvar
367+
if value is not None:
368+
if isinstance(tkvar, tk.StringVar):
369+
print(f"Set StringVar to {repr(value)}")
370+
tkvar.set(str(value))
371+
else:
372+
print(f"Set {type(tkvar).__name__} to {repr(value)}"
373+
f" ({cdivar.data})")
374+
# Assume input already matches type of tk var
375+
tkvar.set(value)
376+
377+
def onWriteValueClicked(self, v_widget: tk.Widget,
378+
tkvar: Union[tk.StringVar, tk.IntVar, tk.DoubleVar], # noqa: E501
379+
cdivar: CDIVar, mapToValue=None):
380+
print(f"TODO: write: {cdivar.className}({repr(tkvar.get())})")
381+
247382
def clearSettingWidgets(self):
248383
for widget in self.cdiSettingWidgets:
249384
widget.grid_forget()

openlcb/cdimemo.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -278,10 +278,11 @@ def toXMLEnd(self):
278278
return memoRepr
279279

280280
def valueMap(self):
281-
# type: () -> Union[dict, None]
281+
# type: () -> Union[dict[str, CDIVar], None]
282282
"""Map each relation in a dict.
283-
Property and value are swapped to make the result a lookup
284-
table where the key is the user-facing caption.
283+
The "property" and "value" are swapped to make the result a
284+
lookup table where the user-facing caption ("value") is used as
285+
the key.
285286
"""
286287
mapMemo = self.getChildByTag("map")
287288
if mapMemo is None:
@@ -315,7 +316,9 @@ def valueMap(self):
315316
logger.warning(f"expected only one value {repr(value)}"
316317
f" (property {repr(prop)}) in relation for"
317318
f" {self.getChildContent('name')}")
318-
results[value] = prop # reverse to make it a lookup by caption
319+
propVar = self.toCDIVar()
320+
propVar.setFromString(prop)
321+
results[value] = propVar # reverse to make it a lookup by caption
319322
return results
320323

321324
def keyMap(self):
@@ -328,7 +331,10 @@ def keyMap(self):
328331
return None
329332
results = OrderedDict()
330333
for k, v in tmp.items():
331-
results[v] = k
334+
vStr = v.value()
335+
assert vStr is not None
336+
vStr = str(vStr)
337+
results[vStr] = k
332338
return results
333339

334340
def getChildByTag(self, tag):

openlcb/cdivar.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,13 @@ def set(self, other: Union['CDIVar', int, float]):
535535
f"Tried to set {self.className} to a(n)"
536536
f" {type(other).__name__}")
537537

538+
def setFromString(self, valueStr):
539+
"""Use this only for types that can be cast from str
540+
(Or implement manual parsing).
541+
"""
542+
value = CLASSNAME_TYPES[self.className](valueStr)
543+
self.set(value)
544+
538545
def getData(self):
539546
return self.data
540547

openlcb/memoryservice.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,13 @@ def __init__(self, nodeID: NodeID, size: int, space: int, address: int,
247247
self.error = None # type: str|None
248248
self.errorCode = None # type: int|None
249249
self.nodeID = nodeID
250+
assert isinstance(size, int)
250251
self.size = size
252+
if isinstance(space, MemorySpace):
253+
space = space.value
254+
assert isinstance(space, int)
251255
self.space = space
256+
assert isinstance(address, int)
252257
self.address = address
253258
self.rejectedReply = rejectedReply
254259
self.dataReply = dataReply

0 commit comments

Comments
 (0)