Skip to content

Commit

Permalink
Merge pull request #54 from Hierosoft/node-id-detection
Browse files Browse the repository at this point in the history
Node id detection
  • Loading branch information
bobjacobsen authored Jan 8, 2025
2 parents 4a9f669 + 4b94c25 commit 41837c4
Show file tree
Hide file tree
Showing 26 changed files with 571 additions and 82 deletions.
5 changes: 3 additions & 2 deletions examples/example_cdi_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,10 @@ def printDatagram(memo):
# accumulate the CDI information
resultingCDI = []

# callbacks to get results of memory read

def memoryReadSuccess(memo):
"""createcallbacks to get results of memory read
"""Handle a successful read
Invoked when the memory read successfully returns,
this queues a new read until the entire CDI has been
returned. At that point, it invokes the XML processing below.
Expand Down Expand Up @@ -162,7 +163,7 @@ def __init__(self):
def startElement(self, name, attrs):
print("Start: ", name)
if attrs is not None and attrs :
print(" Atributes: ", attrs.getNames())
print(" Attributes: ", attrs.getNames())

def endElement(self, name):
print(name, "content:", self._flushCharBuffer())
Expand Down
9 changes: 5 additions & 4 deletions examples/example_memory_length_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,17 @@ def printDatagram(memo):

memoryService = MemoryService(datagramService)

# callbacks to get results of memory read

# def memoryReadSuccess(memo):
# """createcallbacks to get results of memory read
#
# """Handle a successful read
#
# Args:
# memo (MemoryReadMemo): Event that was generated.
# """
# print("successful memory read: {}".format(memo.data))
#
#
#
#
# def memoryReadFail(memo):
# print("memory read failed: {}".format(memo.data))

Expand Down
3 changes: 2 additions & 1 deletion examples/example_memory_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,10 @@ def printDatagram(memo):

memoryService = MemoryService(datagramService)

# callbacks to get results of memory read

def memoryReadSuccess(memo):
"""createcallbacks to get results of memory read
"""Handle a successful read
Args:
memo (MemoryReadMemo): Event that was generated.
Expand Down
12 changes: 10 additions & 2 deletions examples/example_node_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
the address and port). Defaults to a hard-coded test
address and port.
'''
import socket

# region same code as other examples
from examples_settings import Settings # do 1st to fix path if no pip install
settings = Settings()
Expand Down Expand Up @@ -44,7 +46,12 @@

s = TcpSocket()
# s.settimeout(30)
s.connect(settings['host'], settings['port'])
try:
s.connect(settings['host'], settings['port'])
except socket.gaierror:
print("Failure accessing {}:{}"
.format(settings.get('host'), settings.get('port')))
raise

print("RR, SR are raw socket interface receive and send;"
" RL, SL are link interface; RM, SM are message interface")
Expand Down Expand Up @@ -94,7 +101,8 @@ def printDatagram(memo):
memoryService = MemoryService(datagramService)


# createcallbacks to get results of memory read
# callbacks to get results of memory read

def memoryReadSuccess(memo):
print("successful memory read: {}".format(memo.data))

Expand Down
2 changes: 1 addition & 1 deletion examples/example_string_serial_interface.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'''
Example of raw socket communications over the physical connection, in this case
a socket.
a serial port.
Usage:
python3 example_string_interface.py [host|host:port]
Expand Down
12 changes: 6 additions & 6 deletions examples/example_tcp_message_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,25 +57,25 @@ def printMessage(msg):
print("RM: {} from {}".format(msg, msg.source))


tcpLinklayer = TcpLink(NodeID(100))
tcpLinklayer.registerMessageReceivedListener(printMessage)
tcpLinklayer.linkPhysicalLayer(sendToSocket)
tcpLinkLayer = TcpLink(NodeID(100))
tcpLinkLayer.registerMessageReceivedListener(printMessage)
tcpLinkLayer.linkPhysicalLayer(sendToSocket)

#######################

# have the socket layer report up to bring the link layer up and get an alias
print(" SL : link up")
tcpLinklayer.linkUp()
tcpLinkLayer.linkUp()

# send an VerifyNodes message to provoke response
message = Message(MTI.Verify_NodeID_Number_Global,
NodeID(settings['localNodeID']), None)
print("SM: {}".format(message))
tcpLinklayer.sendMessage(message)
tcpLinkLayer.sendMessage(message)

# process resulting activity
while True:
received = s.receive()
print(" RR: {}".format(received))
# pass to link processor
tcpLinklayer.receiveListener(received)
tcpLinkLayer.receiveListener(received)
85 changes: 65 additions & 20 deletions examples/examples_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from collections import OrderedDict

from examples_settings import Settings
from openlcb.tcplink.mdnsconventions import id_from_tcp_service_name

zeroconf_enabled = False
try:
Expand Down Expand Up @@ -90,7 +91,7 @@ class MainForm(ttk.Frame):
key and the Button instance is the value.
example_modules (OrderedDict[str]): The example
module name is the key, and the full path is the value. If
examples are made modular, the value will not be nessary, but
examples are made modular, the value will not be necessary, but
for now just run the file in another Python instance (See
run_example method).
Expand Down Expand Up @@ -291,7 +292,10 @@ def gui(self, parent):
self.row = 0
self.add_field("service_name",
"TCP Service name (optional, sets host&port)",
gui_class=ttk.Combobox, tooltip="")
gui_class=ttk.Combobox, tooltip="",
command=self.set_id_from_name,
command_text="Copy digits to Far Node ID")
self.fields["service_name"].button.configure(state=tk.DISABLED)
self.fields["service_name"].var.trace('w', self.on_service_name_change)
self.add_field("host", "IP address/hostname",
command=self.detect_hosts,
Expand Down Expand Up @@ -334,8 +338,8 @@ def gui(self, parent):
self.add_field(
"farNodeID", "Far Node ID",
gui_class=ttk.Combobox,
# command=self.detect_nodes, # TODO: finish detect_nodes & use
# command_text="Detect", # TODO: finish detect_nodes & use
command=self.detect_nodes, # TODO: finish detect_nodes & use
command_text="Detect", # TODO: finish detect_nodes & use
)

self.add_field(
Expand All @@ -345,6 +349,17 @@ def gui(self, parent):
command_text="Default",
)

self.add_field(
"timeout", "Remote nodes timeout (seconds)",
gui_class=ttk.Entry,
)

self.add_field(
"trace", "Remote nodes logging",
gui_class=ttk.Checkbutton,
text="Trace",
)

# The status widget is the only widget other than self which
# is directly inside the parent widget (forces it to bottom):
self.statusLabel = ttk.Label(self.parent)
Expand All @@ -362,28 +377,45 @@ def gui(self, parent):
# self.rowconfigure(row, weight=1)
# self.rowconfigure(self.row_count-1, weight=1) # make last row expand

def set_id_from_name(self):
id = self.get_id_from_name(update_button=True)
if not id:
return
self.fields['farNodeID'].var.set(id)

def get_id_from_name(self, update_button=False):
lcc_id = id_from_tcp_service_name(self.fields['service_name'].var.get())
if update_button:
if not lcc_id:
self.fields["service_name"].button.configure(state=tk.DISABLED)
else:
self.fields["service_name"].button.configure(state=tk.NORMAL)
return lcc_id

def on_service_name_change(self, index, value, op):
key = self.fields['service_name'].get()
_ = self.get_id_from_name(update_button=True)
info = self.detected_services.get(key)
if not info:
# The user may be typing, so don't spam screen with messages,
# just ignore incomplete entries.
return
# We got info, so use the info to set *other* fields:
self.fields['host'].set(info['server'])
self.fields['host'].set(info['server'].rstrip("."))
# ^ Remove trailing "." to prevent getaddrinfo failed.
self.fields['port'].set(info['port'])
self.set_status("Hostname & Port have been set ({server}:{port})"
.format(**info))

def add_field(self, key, text, gui_class=ttk.Entry, command=None,
command_text=None, tooltip=None):
def add_field(self, key, caption, gui_class=ttk.Entry, command=None,
command_text=None, tooltip=None, text=None):
"""Generate a uniform data field that may or may not affect a setting.
The row(s) for the data field will start at self.row, and self.row will
be incremented for (each) row added by this function.
Args:
text (str): Text for the label.
caption (str): Text for the label.
key (str): Key to store the widget.
gui_class (Misc): The ttk widget class or function to use to create
the data entry widget (field.widget).
Expand All @@ -392,6 +424,8 @@ def add_field(self, key, text, gui_class=ttk.Entry, command=None,
tooltip (str, optional): Add a tooltip tk.Label as field.tooltip
with this text. Added even if "". Defaults to None (not added
in that case).
text (str, optional): Text on the input widget itself (only
applies to gui_class Checkbutton).
"""
# self.row should already be set to an empty row.
self.column = 0 # Return to beginning of row
Expand All @@ -404,16 +438,27 @@ def add_field(self, key, text, gui_class=ttk.Entry, command=None,
raise ValueError("command is required for command_caption.")

field = DataField()
field.label = ttk.Label(self, text=text)
field.label = ttk.Label(self, text=caption)
field.label.grid(row=self.row, column=self.column, **self.grid_args)
self.host_column = self.column
self.column += 1
self.fields[key] = field
field.var = tk.StringVar(self.w1)
field.widget = gui_class(
self,
textvariable=field.var,
)
if gui_class in (ttk.Checkbutton, tk.Checkbutton):
field.var = tk.BooleanVar(self.w1)
# field.var.set(True)
field.widget = gui_class(
self,
# onvalue=True,
# offvalue=False,
variable=field.var,
text=text,
)
else:
field.var = tk.StringVar(self.w1)
field.widget = gui_class(
self,
textvariable=field.var,
)
field.widget.grid(row=self.row, column=self.column, **self.grid_args)
self.column += 1

Expand Down Expand Up @@ -550,14 +595,14 @@ def main():
window_h,
)) # WxH+X+Y format
root.minsize = (window_w, window_h)
mainform = MainForm(root)
mainform.master.title("Python OpenLCB Examples")
main_form = MainForm(root)
main_form.master.title("Python OpenLCB Examples")
try:
mainform.mainloop()
main_form.mainloop()
finally:
if mainform.zeroconf:
mainform.zeroconf.close()
mainform.zeroconf = None
if main_form.zeroconf:
main_form.zeroconf.close()
main_form.zeroconf = None
return 0


Expand Down
7 changes: 6 additions & 1 deletion examples/examples_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"timeout": 0.5, # Such as for example_remote_nodes.py
"device": "/dev/cu.usbmodemCC570001B1",
# ^ serial device such as for example_string_serial_interface.py
# "service_name": "", # mdns service name (maybe more than 1 on host)
# "service_name": "", # MDNS service name (maybe more than 1 on host)
# ^ service_name isn't saved here, since it is not used by LCC
# examples (See examples_gui instead, which finds it dynamically,
# and where it is associated with a host and port).
Expand Down Expand Up @@ -140,6 +140,11 @@ def __next__(self):
self._iterate_i += 1
return key

def get(self, key):
if key not in self:
return None
return self[key]

def keys(self):
"""Return the keys iterator from the settings dictionary.
Expand Down
19 changes: 19 additions & 0 deletions openlcb/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import re


hex_pairs_rc = re.compile(r"^([0-9A-Fa-f]{2})+$")
# {2}: Exactly two characters found (only match if pair)
# +: at least one match plus 0 or more additional matches


def only_hex_pairs(value):
"""Check if string contains only machine-readable hex pairs.
See openlcb.conventions submodule for LCC ID dot notation
functions (less restrictive).
"""
return hex_pairs_rc.fullmatch(value)


def emit_cast(value):
"""Show type and value, such as for debug output."""
return "{}({})".format(type(value).__name__, repr(value))
Loading

0 comments on commit 41837c4

Please sign in to comment.