From 6b79cc657250cd91eca4340204964ca3a2be86a2 Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:24:20 -0400 Subject: [PATCH 01/16] Fix redefined method masking a test. Use PEP8 more. --- tests/test_nodestore.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_nodestore.py b/tests/test_nodestore.py index 57b13b8..0b6dd94 100644 --- a/tests/test_nodestore.py +++ b/tests/test_nodestore.py @@ -5,11 +5,12 @@ from openlcb.node import Node from openlcb.nodeid import NodeID + class TestNodeStoreClass(unittest.TestCase): def testIsPresent(self) : dut = NodeStore() - + node = Node(NodeID(120)) dut.store(node) @@ -17,9 +18,9 @@ def testIsPresent(self) : self.assertEqual(dut.isPresent(NodeID(123)), False, "is present") - def testIsPresent(self) : + def testAsArray(self) : dut = NodeStore() - + node1 = Node(NodeID(120)) dut.store(node1) @@ -29,6 +30,5 @@ def testIsPresent(self) : self.assertEqual(dut.asArray(), [node1, node2], "as array") - if __name__ == '__main__': unittest.main() From 17d501ef1e6b1eb113b478c7984b9fbf1c6ce5ab Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:26:00 -0400 Subject: [PATCH 02/16] Ignore some visual-only and PEP8 rules (also, E226 seems to be in flux as used in args). --- python-openlcb.code-workspace | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/python-openlcb.code-workspace b/python-openlcb.code-workspace index 5dc94bb..b9cec04 100644 --- a/python-openlcb.code-workspace +++ b/python-openlcb.code-workspace @@ -6,6 +6,12 @@ ], "settings": { "rewrap.wrappingColumn": 79, - "autoDocstring.docstringFormat": "google" + "autoDocstring.docstringFormat": "google", + "flake8.args": [ + "--ignore=E203,E226,E701" + ], + "ruff.lint.args": [ + "--ignore=E701" + ] } } \ No newline at end of file From 82c3b3b5195bf5d5d8f5f3956b4f66b750d89d8b Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:36:40 -0400 Subject: [PATCH 03/16] Ignore whitespace before ")". --- python-openlcb.code-workspace | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-openlcb.code-workspace b/python-openlcb.code-workspace index b9cec04..8110760 100644 --- a/python-openlcb.code-workspace +++ b/python-openlcb.code-workspace @@ -8,7 +8,7 @@ "rewrap.wrappingColumn": 79, "autoDocstring.docstringFormat": "google", "flake8.args": [ - "--ignore=E203,E226,E701" + "--ignore=E203,E226,E701,E202" ], "ruff.lint.args": [ "--ignore=E701" From 65d5aaf89c381105c2401d92e9d3e7611b50d8a0 Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:38:07 -0400 Subject: [PATCH 04/16] Ignore extra space after operator. --- python-openlcb.code-workspace | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-openlcb.code-workspace b/python-openlcb.code-workspace index 8110760..849b430 100644 --- a/python-openlcb.code-workspace +++ b/python-openlcb.code-workspace @@ -8,7 +8,7 @@ "rewrap.wrappingColumn": 79, "autoDocstring.docstringFormat": "google", "flake8.args": [ - "--ignore=E203,E226,E701,E202" + "--ignore=E203,E226,E701,E202,E222" ], "ruff.lint.args": [ "--ignore=E701" From 8228bcf4b8ca5656f402f884d35bfeecfdf3a4d8 Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:41:28 -0400 Subject: [PATCH 05/16] Ignore multiple spaces before operator. --- python-openlcb.code-workspace | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-openlcb.code-workspace b/python-openlcb.code-workspace index 849b430..1d6e288 100644 --- a/python-openlcb.code-workspace +++ b/python-openlcb.code-workspace @@ -8,7 +8,7 @@ "rewrap.wrappingColumn": 79, "autoDocstring.docstringFormat": "google", "flake8.args": [ - "--ignore=E203,E226,E701,E202,E222" + "--ignore=E203,E226,E701,E202,E222,E221" ], "ruff.lint.args": [ "--ignore=E701" From 47008796697d04ae0c5d664551a23be34ce1486a Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:44:43 -0400 Subject: [PATCH 06/16] =?UTF-8?q?Allow=20newline=20before=20binary=20opera?= =?UTF-8?q?tor=20(pep-0008=20says=20"Knuth=E2=80=99s=20style=20is=20sugges?= =?UTF-8?q?ted",=20so=20W503=20is=20deprecated).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- python-openlcb.code-workspace | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-openlcb.code-workspace b/python-openlcb.code-workspace index 1d6e288..2cfde97 100644 --- a/python-openlcb.code-workspace +++ b/python-openlcb.code-workspace @@ -8,7 +8,7 @@ "rewrap.wrappingColumn": 79, "autoDocstring.docstringFormat": "google", "flake8.args": [ - "--ignore=E203,E226,E701,E202,E222,E221" + "--ignore=E203,E226,E701,E202,E222,E221,W503" ], "ruff.lint.args": [ "--ignore=E701" From 090ed035c792f7b2060b77899e6f41b84f2fb374 Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:46:01 -0400 Subject: [PATCH 07/16] Ignore extra space after comma (E241). --- python-openlcb.code-workspace | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-openlcb.code-workspace b/python-openlcb.code-workspace index 2cfde97..137ec61 100644 --- a/python-openlcb.code-workspace +++ b/python-openlcb.code-workspace @@ -8,7 +8,7 @@ "rewrap.wrappingColumn": 79, "autoDocstring.docstringFormat": "google", "flake8.args": [ - "--ignore=E203,E226,E701,E202,E222,E221,W503" + "--ignore=E203,E226,E701,E202,E222,E221,W503,E241" ], "ruff.lint.args": [ "--ignore=E701" From 740f3fc74853ece4f71d0f9405205aed66e00dc7 Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:12:51 -0400 Subject: [PATCH 08/16] Fix undefined nodeId in receivedPart, and add missing substitution (change [] to {}). FIXME: undefined name "messagePart". Use PEP8 more (but ignore E501 line too long in some cases). FIXME: Consider deleting openlcb/tcplink/__init__.py then in a separate commit (So git can track the rename operation), rename openlcb/tcplink/tcplink.py to openlcb/tcplink/__init__.py. --- openlcb/tcplink/tcplink.py | 132 +++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 57 deletions(-) diff --git a/openlcb/tcplink/tcplink.py b/openlcb/tcplink/tcplink.py index d04db87..56b8c02 100644 --- a/openlcb/tcplink/tcplink.py +++ b/openlcb/tcplink/tcplink.py @@ -19,24 +19,40 @@ import logging import time + class TcpLink(LinkLayer): + """A TCP link layer. + + Attributes: + accumulatedData (list): input accumulated until an entire message is + present. - def __init__(self, localNodeID): # a NodeID + Args: + localNodeID (NodeID): The node ID of the Configuration Tool or other + software-defined node connecting to the LCC network via TCP. + """ + + def __init__(self, localNodeID): + # See class docstring for argument(s) and attributes. self.localNodeID = localNodeID self.linkCall = None self.accumulatedParts = {} self.nextInternallyAssignedNodeID = 1 - self.accumulatedData = [] # input accumulated until an entire message is present + self.accumulatedData = [] + + def linkPhysicalLayer(self, lpl): + """Register the handler for when the layer is up. - def linkPhysicalLayer(self, lpl): # usually a socket connection send() method + Args: + lpl (function): A handler that accepts a bytes object, usually a + socket connection send() method. + """ self.linkCall = lpl - - + def receiveListener(self, inputData): # [] input - """ - Receives bytes from lower level and - accumulates them into individual message parts. - + """Receives bytes from lower level + and accumulates them into individual message parts. + Args: inputData ([int]) : next chunk of the input stream """ @@ -44,61 +60,66 @@ def receiveListener(self, inputData): # [] input # Now check it if has one or more complete message. while len(self.accumulatedData) > 0 : # first, see if entire prefix is present - if len(self.accumulatedData) < 17 : # 2+3+6+6 + if len(self.accumulatedData) < 17 : # 2+3+6+6 # not yet, wait for more return - flags = (self.accumulatedData[0]<<8) | self.accumulatedData[1] - length =(self.accumulatedData[2]<<16) |(self.accumulatedData[3]<<8) | self.accumulatedData[4] + flags = (self.accumulatedData[0] << 8) | self.accumulatedData[1] + length = (self.accumulatedData[2] << 16) | (self.accumulatedData[3] << 8) | self.accumulatedData[4] # noqa: E501 # check if entire message (part) is present if len(self.accumulatedData) < 5+length : # not yet, wait for more return - + # Check for message indicated bit if (self.accumulatedData[0] & 0x80) == 0x80: # we have a message (part)! Forward for further processing - self.receivedPart(self.accumulatedData[:5+length], flags, length) + self.receivedPart(self.accumulatedData[:5+length], flags, + length) else: # We don't have definitions for link control messages # so log and ignore - logging.info("Found a link control message with flags 0x{:04X} length {}, ignoring" - .format(flags, length)) + logging.info( + "Found a link control message" + " with flags 0x{:04X} length {}, ignoring" + .format(flags, length) + ) # drop that message (part) self.accumulatedData = self.accumulatedData[5+length:] # and repeat - def receivedPart(self, messagePart, flags, length): # messagePart is raw message data - """ - Receives message parts from receiveListener and groups them into - single OpenLCB messages as needed - + def receivedPart(self, messagePart, flags, length): + """Receives message parts from receiveListener + and groups them into single OpenLCB messages as needed + Args: - messagePart ([int]) : a single TCP-level meesage, which - may include all or part of a single OpenLCB message + messagePart (int) : Raw message data. A single TCP-level message, + which may include all or part of a single OpenLCB message. """ # set the source NodeID from the data gatewayNodeID = NodeID(messagePart[5:11]) - + # handle simplest case first - complete message - if (flags& 0x00C0) == 0x00000 : + if (flags & 0x00C0) == 0x00000 : self.forwardMessage(messagePart[17:], gatewayNodeID) return - - # need to accumulate - can be first, middle or last, but not entire message + + # need to accumulate + # - can be first, middle or last, but not entire message key = gatewayNodeID # do we need to have the capture time in here? - if (flags & 0x00C0) == 0x040 : # first - # check for error + if (flags & 0x00C0) == 0x040 : # first + # check for error if self.accumulatedParts.get(key) is not None : # this was a first, but shouldn't have been - logging.warn("Found a first part from [] while already accumulating" - .format(nodeId)) + logging.warn("Found a first part from {}" + " while already accumulating" + "".format(gatewayNodeID)) # start over # start accumulation self.accumulatedParts[key] = [] # accumulate next part self.accumulatedParts[key].extend(messagePart[17:]) # is the accumulation complete? - if (flags & 0x00C0) == 0x0080 : # first + if (flags & 0x00C0) == 0x0080 : # first # yes, forward to upper layer messageBytes = self.accumulatedParts[key] self.forwardMessage(messageBytes, gatewayNodeID) @@ -106,14 +127,14 @@ def receivedPart(self, messagePart, flags, length): # messagePart is raw message del self.accumulatedParts[key] # wait for next part return - - def forwardMessage(self, messageBytes, gatewayNodeID) : # not sure why gatewayNodeID useful here... + + def forwardMessage(self, messageBytes, gatewayNodeID) : # not sure why gatewayNodeID useful here... # noqa: E501 """ Receives single message from receivedPart, converts it in a Message object, and forwards to listeners - + Args: - messageBytes ([int]) : the bytes making up a + messageBytes ([int]) : the bytes making up a single OpenLCB message, starting with the MTI """ # extract MTI @@ -130,11 +151,11 @@ def forwardMessage(self, messageBytes, gatewayNodeID) : # not sure why gatewayNo message = Message(mti, sourceNodeID, destNodeID, data) # forward to listeners self.fireListeners(message) - + def linkUp(self): """ Link started, notify upper layers - """ + """ msg = Message(MTI.Link_Layer_Up, NodeID(0), None, []) self.fireListeners(msg) @@ -148,31 +169,31 @@ def linkRestarted(self): def linkDown(self): """ Link dropped, notify upper layers - """ + """ msg = Message(MTI.Link_Layer_Down, NodeID(0), None, []) self.fireListeners(msg) def sendMessage(self, message): """ - The message level calls this with an OpenLCB + The message level calls this with an OpenLCB message. That is then converted to a byte stream and forwarded to the TCP socket layer. """ - + mti = message.mti - outputBytes = [0x80, 0x00] # flags - + outputBytes = [0x80, 0x00] # flags + length = 12+2+6+len(message.data) if mti.addressPresent() : length = length+6 l0 = (length & 0xFF0000) >> 16 l1 = (length & 0xFF00) >> 8 l2 = (length & 0xFF) - outputBytes.extend([l0,l1,l2]) - + outputBytes.extend([l0, l1, l2]) + outputBytes.extend(self.localNodeID.toArray()) - + t = round(time.time() * 1000) t0 = (t & 0xFF0000000000) >> 40 t1 = (t & 0xFF00000000) >> 32 @@ -180,20 +201,17 @@ def sendMessage(self, message): t3 = (t & 0xFF0000) >> 16 t4 = (t & 0xFF00) >> 8 t5 = (t & 0xFF) - outputBytes.extend([t0,t1,t2,t3,t4,t5]) - + outputBytes.extend([t0, t1, t2, t3, t4, t5]) + m0 = (mti.value & 0xFF00) >> 8 m1 = (mti.value & 0xFF) outputBytes.extend([m0, m1]) - + outputBytes.extend(message.source.toArray()) - - if mti.addressPresent() : outputBytes.extend(message.destination.toArray()) - + + if mti.addressPresent() : + outputBytes.extend(message.destination.toArray()) + outputBytes.extend(message.data) - - self.linkCall(outputBytes) - - - + self.linkCall(outputBytes) From 139528eb15855df995d8cf87da1ca9cea8b3fabb Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:34:04 -0400 Subject: [PATCH 09/16] Fix order of operation converting byte list to str in receive: b'' cannot join str (would raise a TypeError). Use PEP8 more (including some Google-style docstrings). TODO: Add a (virtual) test for receive. --- openlcb/canbus/seriallink.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/openlcb/canbus/seriallink.py b/openlcb/canbus/seriallink.py index 319ccfc..5d32c68 100644 --- a/openlcb/canbus/seriallink.py +++ b/openlcb/canbus/seriallink.py @@ -6,16 +6,32 @@ MSGLEN = 35 + class SerialLink: def __init__(self): pass - + def connect(self, device, baudrate=230400): + """Connect to a serial port. + + Args: + device (str): A string that identifies a serial port for the + serial.Serial constructor. + baudrate (int, optional): Desired serial speed. Defaults to + 230400 bits per second. + """ self.port = serial.Serial(device, baudrate) - self.port.reset_input_buffer() # drop anything that's just sitting there already + self.port.reset_input_buffer() # drop anything that's just sitting there already # noqa: E501 - # send a single string def send(self, string): + """send a single string + + Args: + string (str): Any string. + + Raises: + RuntimeError: If the string couldn't be written to the port. + """ msg = string.encode('ascii') totalsent = 0 while totalsent < len(msg[totalsent:]): @@ -30,6 +46,9 @@ def receive(self): response. - This makes it nicer to display the raw data. - Note that the response may end with a partial frame. + + Returns: + str: A GridConnect frame as a string. ''' chunks = [] bytes_recd = 0 @@ -41,4 +60,4 @@ def receive(self): bytes_recd = bytes_recd + len(chunk) if 0x3B in chunk: break - return b''.join(chunks).decode("utf-8") + return (b''.join(chunks)).decode("utf-8") From cf4a3759a8c2248b51666757aaba6813cbbe864e Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:45:12 -0400 Subject: [PATCH 10/16] Add docstrings, and make some existing ones Google Style such as for IDE hinting and Sphinx documentation generation. Use PEP8 more (mostly spacing, though several spacing rules are ignored by flake8 --ignore=... in an earlier commit to python-openlcb.code-workspace. flake8 and pylance [via ruff] discovered some legitimate bugs noted in earlier commits, so reducing style warnings helps narrow them down). --- example_cdi_access.py | 68 +++-- example_datagram_transfer.py | 6 +- example_frame_interface.py | 4 +- example_memory_transfer.py | 8 +- example_message_interface.py | 4 +- example_node_implementation.py | 18 +- example_remote_nodes.py | 67 +++-- example_tcp_message_interface.py | 2 + openlcb/canbus/canframe.py | 2 +- openlcb/canbus/canlink.py | 20 +- openlcb/canbus/canphysicallayergridconnect.py | 6 +- openlcb/canbus/tcpsocket.py | 9 +- openlcb/datagramservice.py | 12 +- openlcb/linklayer.py | 5 +- openlcb/localeventstore.py | 13 +- openlcb/localnodeprocessor.py | 4 +- openlcb/memoryservice.py | 9 +- openlcb/message.py | 11 +- openlcb/mti.py | 15 +- openlcb/node.py | 1 + openlcb/nodestore.py | 28 +- openlcb/pip.py | 13 +- openlcb/processor.py | 16 +- openlcb/remotenodeprocessor.py | 85 +++--- openlcb/remotenodestore.py | 31 +- openlcb/snip.py | 10 +- openlcb/tcplink/tcpsocket.py | 13 +- tests/test_canlink.py | 34 +-- tests/test_canphysicallayergridconnect.py | 5 +- tests/test_datagramservice.py | 1 - tests/test_linklayer.py | 6 +- tests/test_localeventstore.py | 8 +- tests/test_localnodeprocessor.py | 4 +- tests/test_memoryservice.py | 29 +- tests/test_remotenodeprocessor.py | 110 ++++--- tests/test_remotenodestore.py | 52 ++-- tests/test_tcplink.py | 283 +++++++++--------- 37 files changed, 577 insertions(+), 435 deletions(-) diff --git a/example_cdi_access.py b/example_cdi_access.py index d080a52..f7b890c 100644 --- a/example_cdi_access.py +++ b/example_cdi_access.py @@ -13,7 +13,9 @@ from openlcb.canbus.tcpsocket import TcpSocket -from openlcb.canbus.canphysicallayergridconnect import CanPhysicalLayerGridConnect +from openlcb.canbus.canphysicallayergridconnect import ( + CanPhysicalLayerGridConnect, +) from openlcb.canbus.canlink import CanLink from openlcb.nodeid import NodeID from openlcb.datagramservice import ( @@ -28,7 +30,7 @@ host = "192.168.16.212" port = 12021 localNodeID = "05.01.01.01.03.01" -#farNodeID = "09.00.99.03.00.35" +# farNodeID = "09.00.99.03.00.35" farNodeID = "02.01.57.00.04.9C" # region same code as other examples @@ -68,11 +70,12 @@ def usage(): s.connect(host, port) -#print("RR, SR are raw socket interface receive and send;" +# print("RR, SR are raw socket interface receive and send;" # " RL, SL are link interface; RM, SM are message interface") + def sendToSocket(string): - #print(" SR: {}".format(string.strip())) + # print(" SR: {}".format(string.strip())) s.send(string) @@ -80,21 +83,23 @@ def printFrame(frame): # print(" RL: {}".format(frame)) pass + def printMessage(message): - #print("RM: {} from {}".format(message, message.source)) + # print("RM: {} from {}".format(message, message.source)) pass + def printDatagram(memo): - """create a call-back to print datagram contents when received + """A call-back for when datagrams received Args: - memo (_type_): _description_ + DatagramReadMemo: The datagram object Returns: - bool: Always False (True would mean we sent a reply to this datagram, - but let MemoryService do that). + bool: Always False (True would mean we sent a reply to the datagram, + but let the MemoryService do that). """ - #print("Datagram receive call back: {}".format(memo.data)) + # print("Datagram receive call back: {}".format(memo.data)) return False @@ -113,25 +118,26 @@ def printDatagram(memo): memoryService = MemoryService(datagramService) - # accumulate the CDI information resultingCDI = [] -# 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. + def memoryReadSuccess(memo): """createcallbacks to get results of memory 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. + Args: memo (_type_): _description_ """ - #print("successful memory read: {}".format(memo.data)) - + # print("successful memory read: {}".format(memo.data)) + global resultingCDI # is this done? - if len(memo.data) == 64 and not 0 in memo.data: + if len(memo.data) == 64 and 0 not in memo.data: # save content resultingCDI += memo.data # update the address @@ -140,7 +146,7 @@ def memoryReadSuccess(memo): memoryService.requestMemoryRead(memo) else : # and we're done! - # save content + # save content resultingCDI += memo.data # concert resultingCDI to a string up to 1st zero cdiString = "" @@ -151,17 +157,19 @@ def memoryReadSuccess(memo): # and process that processXML(cdiString) - + # done - + + def memoryReadFail(memo): print("memory read failed: {}".format(memo.data)) + ####################### # The XML parsing section. -# +# # This creates a handler object that just prints -# information as it's presented. +# information as it's presented. # # Since `characters` can be called multiple times # in a row, we buffer up the characters until the `endElement` @@ -169,8 +177,9 @@ def memoryReadFail(memo): import xml.sax -# define XML SAX callbacks in a handler object + class MyHandler(xml.sax.handler.ContentHandler): + """XML SAX callbacks in a handler object""" def __init__(self): self._charBuffer = [] @@ -192,17 +201,24 @@ def _flushCharBuffer(self): def characters(self, data): self._charBuffer.append(data) + handler = MyHandler() -# process the XML and invoke callbacks + def processXML(content) : + """process the XML and invoke callbacks + + Args: + content (_type_): _description_ + """ xml.sax.parseString(content, handler) print("\nParser done") + ####################### # have the socket layer report up to bring the link layer up and get an alias -#print(" SL : link up") +# print(" SL : link up") canPhysicalLayerGridConnect.physicalLayerUp() @@ -228,6 +244,6 @@ def memoryRead(): # process resulting activity while True: received = s.receive() - #print(" RR: {}".format(received.strip())) + # print(" RR: {}".format(received.strip())) # pass to link processor canPhysicalLayerGridConnect.receiveString(received) diff --git a/example_datagram_transfer.py b/example_datagram_transfer.py index beec326..157b749 100644 --- a/example_datagram_transfer.py +++ b/example_datagram_transfer.py @@ -12,7 +12,9 @@ import threading from openlcb.canbus.tcpsocket import TcpSocket -from openlcb.canbus.canphysicallayergridconnect import CanPhysicalLayerGridConnect +from openlcb.canbus.canphysicallayergridconnect import ( + CanPhysicalLayerGridConnect, +) from openlcb.canbus.canlink import CanLink from openlcb.nodeid import NodeID from openlcb.datagramservice import ( @@ -101,7 +103,7 @@ def datagramReceiver(memo): """A call-back for when datagrams received Args: - _type_: _description_ + DatagramReadMemo: The datagram object Returns: bool: Always True (means we sent the reply to this datagram) diff --git a/example_frame_interface.py b/example_frame_interface.py index 4d9c8de..4870a9d 100644 --- a/example_frame_interface.py +++ b/example_frame_interface.py @@ -12,7 +12,9 @@ ''' from openlcb.canbus.tcpsocket import TcpSocket -from openlcb.canbus.canphysicallayergridconnect import CanPhysicalLayerGridConnect +from openlcb.canbus.canphysicallayergridconnect import ( + CanPhysicalLayerGridConnect, +) from openlcb.canbus.canframe import CanFrame from openlcb.canbus.controlframe import ControlFrame diff --git a/example_memory_transfer.py b/example_memory_transfer.py index 90915c0..4d12d96 100644 --- a/example_memory_transfer.py +++ b/example_memory_transfer.py @@ -13,7 +13,9 @@ from openlcb.canbus.tcpsocket import TcpSocket -from openlcb.canbus.canphysicallayergridconnect import CanPhysicalLayerGridConnect +from openlcb.canbus.canphysicallayergridconnect import ( + CanPhysicalLayerGridConnect, +) from openlcb.canbus.canlink import CanLink from openlcb.nodeid import NodeID from openlcb.datagramservice import ( @@ -102,7 +104,7 @@ def printDatagram(memo): """create a call-back to print datagram contents when received Args: - memo (_type_): _description_ + memo (DatagramReadMemo): The datagram received Returns: bool: Always False (True would mean we sent a reply to this datagram, @@ -121,7 +123,7 @@ def memoryReadSuccess(memo): """createcallbacks to get results of memory read Args: - memo (_type_): _description_ + memo (MemoryReadMemo): Event that was generated. """ print("successful memory read: {}".format(memo.data)) diff --git a/example_message_interface.py b/example_message_interface.py index 661045c..96f723e 100644 --- a/example_message_interface.py +++ b/example_message_interface.py @@ -13,7 +13,9 @@ ''' from openlcb.canbus.tcpsocket import TcpSocket -from openlcb.canbus.canphysicallayergridconnect import CanPhysicalLayerGridConnect +from openlcb.canbus.canphysicallayergridconnect import ( + CanPhysicalLayerGridConnect, +) from openlcb.canbus.canlink import CanLink from openlcb.nodeid import NodeID from openlcb.message import Message diff --git a/example_node_implementation.py b/example_node_implementation.py index 60b4f1d..c5e04a1 100644 --- a/example_node_implementation.py +++ b/example_node_implementation.py @@ -11,7 +11,9 @@ ''' from openlcb.canbus.tcpsocket import TcpSocket -from openlcb.canbus.canphysicallayergridconnect import CanPhysicalLayerGridConnect +from openlcb.canbus.canphysicallayergridconnect import ( + CanPhysicalLayerGridConnect, +) from openlcb.canbus.canlink import CanLink from openlcb.nodeid import NodeID from openlcb.datagramservice import DatagramService @@ -99,7 +101,7 @@ def printDatagram(memo): """create a call-back to print datagram contents when received Args: - memo (_type_): _description_ + memo (DatagramReadMemo): The datagram received Returns: bool: Always False (True would mean we sent a reply to the datagram, @@ -136,12 +138,22 @@ def memoryReadFail(memo): localNodeProcessor = LocalNodeProcessor(canLink, localNode) canLink.registerMessageReceivedListener(localNodeProcessor.process) -# create a listener to identify connected nodes + def displayOtherNodeIds(message) : + """Listener to identify connected nodes + + Args: + message (_type_): _description_ + """ + print("[displayOtherNodeIds] type(message): {}" + "".format(type(message).__name__)) if message.mti == MTI.Verified_NodeID : print("Detected farNodeID is {}".format(message.source)) + + canLink.registerMessageReceivedListener(displayOtherNodeIds) + ####################### # have the socket layer report up to bring the link layer up and get an alias diff --git a/example_remote_nodes.py b/example_remote_nodes.py index ffe9d29..545cbf3 100644 --- a/example_remote_nodes.py +++ b/example_remote_nodes.py @@ -11,10 +11,12 @@ address and port. ''' -from openlcb.canbus.canphysicallayergridconnect import CanPhysicalLayerGridConnect -from openlcb.canbus.canframe import CanFrame +from openlcb.canbus.canphysicallayergridconnect import ( + CanPhysicalLayerGridConnect, +) +# from openlcb.canbus.canframe import CanFrame from openlcb.canbus.canlink import CanLink -from openlcb.canbus.controlframe import ControlFrame +# from openlcb.canbus.controlframe import ControlFrame from openlcb.canbus.tcpsocket import TcpSocket from openlcb.node import Node @@ -39,9 +41,11 @@ # region same code as other examples + def usage(): print(__doc__, file=sys.stderr) + if __name__ == "__main__": # global host # only necessary if this is moved to a main/other function import sys @@ -73,22 +77,28 @@ def usage(): if trace : - print("RR, SR are raw socket interface receive and send; RL, SL are link (frame) interface") + print("RR, SR are raw socket interface receive and send;" + " RL, SL are link (frame) interface") + def sendToSocket(string) : if trace : print(" SR: "+string.strip()) s.send(string) -def receiveFrame(frame) : - if trace: print("RL: "+str(frame) ) - + +def receiveFrame(frame) : + if trace: print("RL: "+str(frame)) + + canPhysicalLayerGridConnect = CanPhysicalLayerGridConnect(sendToSocket) canPhysicalLayerGridConnect.registerFrameReceivedListener(receiveFrame) + def printMessage(msg): if trace: print("RM: {} from {}".format(msg, msg.source)) readQueue.put(msg) + canLink = CanLink(NodeID(localNodeID)) canLink.linkPhysicalLayer(canPhysicalLayerGridConnect) canLink.registerMessageReceivedListener(printMessage) @@ -110,13 +120,16 @@ def printMessage(msg): remoteNodeStore = RemoteNodeStore(NodeID(localNodeID)) remoteNodeProcessor = RemoteNodeProcessor(canLink) remoteNodeStore.processors = [remoteNodeProcessor] -canLink.registerMessageReceivedListener(remoteNodeStore.processMessageFromLinkLayer) +canLink.registerMessageReceivedListener( + remoteNodeStore.processMessageFromLinkLayer +) readQueue = Queue() -# put the read on a separate thread + def receiveLoop() : + """put the read on a separate thread""" # bring the CAN level up if trace : print(" SL : link up") canPhysicalLayerGridConnect.physicalLayerUp() @@ -125,27 +138,42 @@ def receiveLoop() : if trace : print(" RR: "+input.strip()) # pass to link processor canPhysicalLayerGridConnect.receiveString(input) -import threading + + +import threading # noqa E402 thread = threading.Thread(daemon=True, target=receiveLoop) -# define a routine for checking tests + def result(arg1, arg2=None, arg3=None, result=True) : - # returns True if OK, False if failed - # If arg1 and arg2 provided - # compare those, and fail if not equal; arg3 is then message - # If only arg1, report it and return result for fail value + """Check and report on test results. + + Args: + arg1: Any value. + arg2: value to compare to arg1. Defaults to None. + arg3: fail if arg1 not equal to arg1; arg3 is then message. Defaults to + None. + result (bool, optional): _description_. Defaults to True. + + Raises: + ValueError: If only arg1 was provided (undefined behavior--in other + words, test itself is wrong not the data). + + Returns: + bool: True if OK, False if failed + """ if arg2 is not None : if arg1 == arg2 : # OK print(arg1) return True else : - print("{} does not equal {}, FAIL".format(arg1, arg2)) + raise ValueError("{} does not equal {}, FAIL".format(arg1, arg2)) return False else: print(arg1) return result + # start the process thread.start() @@ -156,7 +184,7 @@ def result(arg1, arg2=None, arg3=None, result=True) : received = readQueue.get(True, timeout) if trace : print("received: ", received) except Empty: - break + break # send an VerifyNodes message to provoke response print("\nSend Verify NodeID Number Global\n") @@ -170,12 +198,13 @@ def result(arg1, arg2=None, arg3=None, result=True) : received = readQueue.get(True, timeout) if trace : print("received: ", received) except Empty: - break + break # print the resulting node store contents print("\nDiscovered nodes:") for node in remoteNodeStore.asArray() : - print(node, node.snip.manufacturerName, "/", node.snip.userProvidedNodeName) + print(node, node.snip.manufacturerName, "/", + node.snip.userProvidedNodeName) # this ends here, which takes the local node offline diff --git a/example_tcp_message_interface.py b/example_tcp_message_interface.py index 0ba4c62..72624fc 100644 --- a/example_tcp_message_interface.py +++ b/example_tcp_message_interface.py @@ -67,9 +67,11 @@ def sendToSocket(data): print(" SR: {}".format(data.strip())) s.send(data) + def printMessage(msg): print("RM: {} from {}".format(msg, msg.source)) + tcpLinklayer = TcpLink(NodeID(100)) tcpLinklayer.registerMessageReceivedListener(printMessage) tcpLinklayer.linkPhysicalLayer(sendToSocket) diff --git a/openlcb/canbus/canframe.py b/openlcb/canbus/canframe.py index 53a4e9a..0429083 100644 --- a/openlcb/canbus/canframe.py +++ b/openlcb/canbus/canframe.py @@ -20,7 +20,7 @@ def __init__(self, arg1, arg2, arg3=[]): alias = arg3 nodeCode = ((nodeID.nodeId >> ((cid-4)*12)) & 0xFFF) - self.header = ((cid << 12) | nodeCode) << 12 | (alias & 0xFFF) | 0x10_000_000 + self.header = ((cid << 12) | nodeCode) << 12 | (alias & 0xFFF) | 0x10_000_000 # noqa: E501 self.data = [] # two arguments as header, data diff --git a/openlcb/canbus/canlink.py b/openlcb/canbus/canlink.py index a22969a..b9fea97 100644 --- a/openlcb/canbus/canlink.py +++ b/openlcb/canbus/canlink.py @@ -56,9 +56,9 @@ def receiveListener(self, frame): # CanFrame if self.decodeControlFrameFormat(frame) == ControlFrame.LinkUp: self.handleReceivedLinkUp(frame) - elif self.decodeControlFrameFormat(frame) == ControlFrame.LinkRestarted: + elif self.decodeControlFrameFormat(frame) == ControlFrame.LinkRestarted: # noqa: E501 self.handleReceivedLinkRestarted(frame) - elif self.decodeControlFrameFormat(frame) in (ControlFrame.LinkCollision, + elif self.decodeControlFrameFormat(frame) in (ControlFrame.LinkCollision, # noqa: E501 ControlFrame.LinkError): logging.warning("Unexpected error report {:08X}" "".format(frame.header)) @@ -175,7 +175,7 @@ def handleReceivedAMD(self, frame): # CanFrame # check for matching node ID, which is a collision nodeID = NodeID(frame.data) if nodeID == self.localNodeID : - print ("collide") + print("collide") # collision, restart self.processCollision(frame) return @@ -251,13 +251,14 @@ def handleReceivedData(self, frame): # CanFrame # datagram case destAlias = (frame.header & 0x00_FFF_000) >> 12 - + if destAlias in self.aliasToNodeID : destID = self.aliasToNodeID[destAlias] else: destID = NodeID(self.nextInternallyAssignedNodeID) logging.warning("message from unknown dest alias: {}," - " continue with {}".format(str(frame), str(destID))) + " continue with {}" + .format(str(frame), str(destID))) # register that internally-generated nodeID-alias # association self.aliasToNodeID[destAlias] = destID @@ -337,8 +338,8 @@ def handleReceivedData(self, frame): # CanFrame if frame.data[0] & 0x10 == 0: # is end, ship and remove accumulation msg = Message(mti, sourceID, destID, self.accumulator[key]) - # This includes the special case of MTI.Unknown, which needs - # to carry its original MTI value + # This includes the special case of MTI.Unknown, + # which needs to carry its original MTI value if mti is MTI.Unknown : msg.originalMTI = ((frame.header >> 12) & 0xFFF) self.fireListeners(msg) @@ -533,7 +534,6 @@ def processCollision(self, frame) : self.localAlias = self.createAlias12(self.localAliasSeed) self.defineAndReserveAlias() - def sendAliasAllocationSequence(self): '''Send the alias allocation sequence''' self.link.sendCanFrame(CanFrame(7, self.localNodeID, self.localAlias)) @@ -550,7 +550,7 @@ def incrementAlias48(self, oldAlias): of x(i+1) = (2^9+1) x(i) + c where c = 29,741,096,258,473 or 0x1B0CA37A4BA9 ''' - + newProduct = (oldAlias << 9) + oldAlias + (0x1B0CA37A4BA9) maskedProduct = newProduct & 0xFFFF_FFFF_FFFF return maskedProduct @@ -577,7 +577,7 @@ def decodeControlFrameFormat(self, frame): return ControlFrame.Data if (frame.header & 0x4_000_000) != 0: # CID case return ControlFrame.CID - + try: retval = ControlFrame((frame.header >> 12) & 0x2FFFF) return retval # top 1 bit for out-of-band messages diff --git a/openlcb/canbus/canphysicallayergridconnect.py b/openlcb/canbus/canphysicallayergridconnect.py index f7b6045..f0f60b1 100644 --- a/openlcb/canbus/canphysicallayergridconnect.py +++ b/openlcb/canbus/canphysicallayergridconnect.py @@ -55,7 +55,7 @@ def receiveChars(self, data): header = 0 for offset in range(2, 9+1): nextChar = (self.inboundBuffer[index+offset]) - nextByte = (nextChar & 0xF)+9 if nextChar > 0x39 else nextChar & 0xF + nextByte = (nextChar & 0xF)+9 if nextChar > 0x39 else nextChar & 0xF # noqa: E501 header = (header << 4)+nextByte # offset 10 is N # offset 11 might be data, might be ; @@ -65,9 +65,9 @@ def receiveChars(self, data): break # two characters are data byte1 = self.inboundBuffer[index+11+2*dataItem] - part1 = (byte1 & 0xF)+9 if byte1 > 0x39 else byte1 & 0xF + part1 = (byte1 & 0xF)+9 if byte1 > 0x39 else byte1 & 0xF # noqa: E501 byte2 = self.inboundBuffer[index+11+2*dataItem+1] - part2 = (byte2 & 0xF)+9 if byte2 > 0x39 else byte2 & 0xF + part2 = (byte2 & 0xF)+9 if byte2 > 0x39 else byte2 & 0xF # noqa: E501 outData += [part1 << 4 | part2] lastByte += 2 # lastByte is index of ; in this message diff --git a/openlcb/canbus/tcpsocket.py b/openlcb/canbus/tcpsocket.py index ce48e88..7da2fad 100644 --- a/openlcb/canbus/tcpsocket.py +++ b/openlcb/canbus/tcpsocket.py @@ -10,16 +10,16 @@ class TcpSocket: def __init__(self, sock=None): if sock is None: - self.sock = socket.socket( - socket.AF_INET, socket.SOCK_STREAM) + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) else: self.sock = sock def connect(self, host, port): self.sock.connect((host, port)) - # send a single string def send(self, string): + """Send a single string. + """ msg = string.encode('ascii') totalsent = 0 while totalsent < len(msg[totalsent:]): @@ -34,6 +34,9 @@ def receive(self): response. - This makes it nicer to display the raw data. - Note that the response may end with a partial frame. + + Returns: + str: The received bytes decoded into a UTF-8 string. ''' chunks = [] bytes_recd = 0 diff --git a/openlcb/datagramservice.py b/openlcb/datagramservice.py index 8f4899f..9539f66 100644 --- a/openlcb/datagramservice.py +++ b/openlcb/datagramservice.py @@ -145,6 +145,10 @@ def registerDatagramReceivedListener(self, listener): One and only one listener should reply positively or negatively to the datagram and return true. + + Args: + listener (function): A function that accepts a DatagramReadMemo + as an argument. ''' self.listeners.append(listener) @@ -272,8 +276,8 @@ def positiveReplyToDatagram(self, dg, flags=0): """Send a positive reply to a received datagram. Args: - dg (_type_): Datagram memo being responded to. - flags (Optional[__type__]): Flag byte to be returned to sender, see + dg (DatagramReadMemo): Datagram memo being responded to. + flags (Optional[int]): Flag byte to be returned to sender, see Datagram S&TN for meaning. Defaults to 0. """ message = Message(MTI.Datagram_Received_OK, self.linkLayer.localNodeID, @@ -284,8 +288,8 @@ def negativeReplyToDatagram(self, dg, err): """Send a negative reply to a received datagram. Args: - dg (_type_): Datagram memo being responded to. - err (_type_): Error code(s) to be returned to sender, + dg (DatagramReadMemo): Datagram memo being responded to. + err (int): Error code(s) to be returned to sender, see Datagram S&TN for meaning. """ data0 = ((err >> 8) & 0xFF) diff --git a/openlcb/linklayer.py b/openlcb/linklayer.py index 164ac47..4cdf462 100644 --- a/openlcb/linklayer.py +++ b/openlcb/linklayer.py @@ -15,15 +15,14 @@ making multiple copies of a single object. ''' -class LinkLayer: +class LinkLayer: def __init__(self, localNodeID): self.localNodeID = localNodeID def sendMessage(self, msg): - ''' - This is the basic abstract interface + '''This is the basic abstract interface ''' def registerMessageReceivedListener(self, listener): diff --git a/openlcb/localeventstore.py b/openlcb/localeventstore.py index 8483745..8bcbcc3 100644 --- a/openlcb/localeventstore.py +++ b/openlcb/localeventstore.py @@ -5,18 +5,17 @@ class LocalEventStore : ''' def __init__(self) : - self.eventsConsumed=set(()) - self.eventsProduced=set(()) - + self.eventsConsumed = set(()) + self.eventsProduced = set(()) + def consumes(self, id) : self.eventsConsumed.add(id) - + def isConsumed(self, id) : return id in self.eventsConsumed - + def produces(self, id) : self.eventsProduced.add(id) - + def isProduced(self, id) : return id in self.eventsProduced - diff --git a/openlcb/localnodeprocessor.py b/openlcb/localnodeprocessor.py index 1f46e69..93f84fb 100644 --- a/openlcb/localnodeprocessor.py +++ b/openlcb/localnodeprocessor.py @@ -128,14 +128,14 @@ def unrecognizedMTI(self, message, node): by returning OptionalInteractionRejected ''' # FIXME: should be private method. Add _ to start of method name. - + # special case of unknown MTI from lower level unknownAddressed = False originalMTI = 0xFFFF if message.mti == MTI.Unknown : if hasattr(message, "originalMTI") : originalMTI = message.originalMTI - else : + else : logging.error("MTI.Unknown without originalMTI") unknownAddressed = (originalMTI & 0x008 ) != 0 if not (message.mti.addressPresent() or unknownAddressed) : diff --git a/openlcb/memoryservice.py b/openlcb/memoryservice.py index 2bc7f75..8675355 100644 --- a/openlcb/memoryservice.py +++ b/openlcb/memoryservice.py @@ -112,6 +112,9 @@ def requestMemoryRead(self, memo): - If okReply in the memo is triggered, it will be followed by a dataReply. - A rejectedReply will not be followed by a dataReply. + + Args: + memo (MemoryReadMemo): Request to enqueue. ''' # preserve the request self.readMemos.append(memo) @@ -120,7 +123,11 @@ def requestMemoryRead(self, memo): self.requestMemoryReadNext(memo) def requestMemoryReadNext(self, memo): - # send the read request + """send the read request + + Args: + memo (MemoryReadMemo): Request to send. + """ byte6 = False flag = 0 (byte6, flag) = self.spaceDecode(memo.space) diff --git a/openlcb/message.py b/openlcb/message.py index ca2496c..a26b45f 100644 --- a/openlcb/message.py +++ b/openlcb/message.py @@ -8,13 +8,10 @@ class Message: """basic message, with an MTI, source, destination? and data content Args: - mti (_type_): _description_ - source (_type_): _description_ - destination (_type_): Set to None for global. + mti (MTI): Message Type Indicator. + source (NodeID): Message source. + destination (NodeID): Set to None for global. data (list, optional): _description_. Defaults to []. - - Returns: - _type_: _description_ """ def __init__(self, mti, source, destination, data=[]): @@ -47,4 +44,4 @@ def __eq__(lhs, rhs): return True def __hash__(self) : - return self.mti.__hash__() + self.source.__hash__() \ No newline at end of file + return self.mti.__hash__() + self.source.__hash__() diff --git a/openlcb/mti.py b/openlcb/mti.py index 73f0b83..5565a0c 100644 --- a/openlcb/mti.py +++ b/openlcb/mti.py @@ -1,3 +1,6 @@ +""" +Message Type Indicator +""" from enum import Enum @@ -43,16 +46,16 @@ class MTI(Enum): Datagram_Received_OK = 0x0A28 Datagram_Rejected = 0x0A48 - Unknown = 0x0008 # make this addressed so that it;s individually processed + Unknown = 0x0008 # make this addressed so that it;s individually processed # noqa: E501 # These are used for internal signalling and are not present in the MTI # specification. - Link_Layer_Up = 0x2000 # entered Permitted state; needs to be marked global - Link_Layer_Quiesce = 0x2010 # Link needs to be drained, will come back with Link_Layer_Restarted next - Link_Layer_Restarted = 0x2020 # link cycled without change of node state; needs to be marked global - Link_Layer_Down = 0x2030 # entered Inhibited state; needs to be marked global + Link_Layer_Up = 0x2000 # entered Permitted state; needs to be marked global # noqa: E501 + Link_Layer_Quiesce = 0x2010 # Link needs to be drained, will come back with Link_Layer_Restarted next # noqa: E501 + Link_Layer_Restarted = 0x2020 # link cycled without change of node state; needs to be marked global # noqa: E501 + Link_Layer_Down = 0x2030 # entered Inhibited state; needs to be marked global # noqa: E501 - New_Node_Seen = 0x2048 # alias resolution found new node; marked addressed (0x8 bit) + New_Node_Seen = 0x2048 # alias resolution found new node; marked addressed (0x8 bit) # noqa: E501 def priority(self): return (self.value & 0x0C00) >> 10 diff --git a/openlcb/node.py b/openlcb/node.py index 0d20d55..73cf45f 100644 --- a/openlcb/node.py +++ b/openlcb/node.py @@ -17,6 +17,7 @@ from openlcb.snip import SNIP from openlcb.localeventstore import LocalEventStore + class Node: def __init__(self, nodeID, snip=None, pipSet=None): self.id = nodeID diff --git a/openlcb/nodestore.py b/openlcb/nodestore.py index dd03b5b..c22807f 100644 --- a/openlcb/nodestore.py +++ b/openlcb/nodestore.py @@ -1,5 +1,6 @@ from openlcb.nodeid import NodeID + class NodeStore : ''' Store the available Nodes and provide multiple means of retrieval. @@ -7,12 +8,12 @@ class NodeStore : Storage and indexing methods are an internal detail. You can't remove a node; once we know about it, we know about it. ''' - + def __init__(self) : self.byIdMap = {} self.nodes = [] self.processors = [] - + # Store a new node or replace an existing stored node # - Parameter node: new Node content def store(self, node) : @@ -20,23 +21,25 @@ def store(self, node) : self.nodes.append(node) # sort by SNIP user name (ascending, blanks at front) - # This can be too early, when node created but no SNIP yet, so also sort before use in View - self.nodes.sort( key=lambda x: x.snip.userProvidedNodeName, reverse=True) - - def isPresent(self, nodeID ) : + # This can be too early, when node created but no SNIP yet, + # so also sort before use in View + self.nodes.sort(key=lambda x: x.snip.userProvidedNodeName, + reverse=True) + + def isPresent(self, nodeID) : return self.byIdMap.get(nodeID) is not None - + def asArray(self) : return [self.byIdMap[i] for i in self.byIdMap] - + # Retrieve a Node's content from the store # - Parameter is either # userProvidedDescription: string to match SNIP content # nodeID: for direct lookup # - Returns: None if the there's no match def lookup(self, parm) : - if isinstance(parm, NodeID) : - if not parm in self.byIdMap : + if isinstance(parm, NodeID) : + if parm not in self.byIdMap : self.byIdMap[parm] = None return self.byIdMap[parm] # assume parm is string @@ -44,12 +47,11 @@ def lookup(self, parm) : if (node.snip.userProvidedDescription == parm) : return node return None - + # Process a message across all nodes def invokeProcessorsOnNodes(self, message) : publish = False # has any processor returned True? for processor in self.processors : for node in self.byIdMap.values() : - publish = processor.process(message, node) or publish # always invoke Processsor on node first + publish = processor.process(message, node) or publish # always invoke Processsor on node first # noqa: E501 return publish - diff --git a/openlcb/pip.py b/openlcb/pip.py index 4604a72..bd66f44 100644 --- a/openlcb/pip.py +++ b/openlcb/pip.py @@ -53,14 +53,15 @@ def contentsNamesFromList(contents): retval.append(pip.name.replace("_", " ").title()) return retval - def setContentsFromInt(input): - """Get a set of contents from a single numeric input + def setContentsFromInt(bitmask): + """Get a set of contents from a single numeric bitmask Args: - input (_type_): _description_ + bitmask (int): A single number that is the sum of 1 or more + protocol bits. Returns: - set: _description_ + set (PIP): The set of protocol bits derived from the bitmask. """ retVal = [] for val in PIP.list(): @@ -72,10 +73,10 @@ def setContentsFromList(raw): """set contents from a list of numeric inputs Args: - raw (_type_): _description_ + raw (Union[bytes,list[int]]): a list of 1-byte values Returns: - _type_: _description_ + set (PIP): The set of protocol bits derived from the raw data. """ data = 0 if (len(raw) > 0): diff --git a/openlcb/processor.py b/openlcb/processor.py index b86e93c..70d8cb2 100644 --- a/openlcb/processor.py +++ b/openlcb/processor.py @@ -20,7 +20,7 @@ def process(self, message, node=None): Accept a Message, adjust state as needed, possibly reply. Args: - message (_type_): _description_ + message (Message): Message to process. node (Optional[_type_]): _description_. Defaults to None. Returns: @@ -36,11 +36,12 @@ def checkSourceID(self, message, arg): """check whether a message came from a specific nodeID Args: - message (_type_): _description_ - arg (_type_): Node or NodeID + message (Message): A message. + arg (Union[NodeID,int]): NodeID or Node ID int to compare against + message.source. Returns: - _type_: _description_ + bool: Whether the source of message is the given Node ID. """ if isinstance(arg, NodeID): return message.source == arg @@ -53,11 +54,12 @@ def checkDestID(self, message, arg): """check whether a message is addressed to a specific nodeID Args: - message (_type_): _description_ - arg (_type_): _description_ + message (Message): A Message. + arg (Union[NodeID,int]): A Node ID. Returns: - bool: Global messages return False: Not specifically addressed + bool: Whether the message ID matches the arg. Global messages + return False: Not specifically addressed. """ if isinstance(arg, NodeID): return message.destination == arg diff --git a/openlcb/remotenodeprocessor.py b/openlcb/remotenodeprocessor.py index 1c6e116..3170e59 100644 --- a/openlcb/remotenodeprocessor.py +++ b/openlcb/remotenodeprocessor.py @@ -1,39 +1,49 @@ from openlcb.eventid import EventID from openlcb.node import Node -from openlcb.nodeid import NodeID +# from openlcb.nodeid import NodeID from openlcb.message import Message from openlcb.mti import MTI from openlcb.processor import Processor from openlcb.pip import PIP from openlcb.snip import SNIP + class RemoteNodeProcessor(Processor) : - ''' - 'Handle incoming messages for a remote node, AKA an image node, representing some - physical node out on the layout. - - Tracks node status, PIP and SNIP information, but deliberately does not + '''Handle incoming messages for a remote node + AKA an image node representing some physical node out on the layout. + + Tracks node status, PIP and SNIP information, but deliberately does not track memory (config, CDI) contents due to size. ''' def __init__(self, linkLayer=None) : self.linkLayer = linkLayer - + def process(self, message, node) : - # Do a fast drop of messages not to us, from us, or global - note linkLayer up/down are marked as global - if not ( message.mti.isGlobal() + """Do a fast drop of messages not to us, from us, or global + NOTE: linkLayer up/down are marked as global + + Args: + message (Message): A message. + node (Node): Node to match against the message source/destination + ID. + + Returns: + bool: True if message was handled by this method. + """ + if not (message.mti.isGlobal() or self.checkSourceID(message, node) - or self.checkDestID(message, node) ) : + or self.checkDestID(message, node)) : return False - + # if you see anything at all from us, must be in Initialized state - if self.checkSourceID(message, node) : # Sent by node we're processing? - node.state = Node.State.Initialized # in case we came late to the party, must be in Initialized state - + if self.checkSourceID(message, node) : # Sent by node we're processing? # noqa: E501 + node.state = Node.State.Initialized # in case we came late to the party, must be in Initialized state # noqa: E501 + # specific message handling match message.mti : - case MTI.Initialization_Complete | MTI.Initialization_Complete_Simple : + case MTI.Initialization_Complete | MTI.Initialization_Complete_Simple : # noqa: E501 self.initializationComplete(message, node) return True case MTI.Protocol_Support_Reply : @@ -48,10 +58,10 @@ def process(self, message, node) : case MTI.Simple_Node_Ident_Info_Reply : self.simpleNodeIdentInfoReply(message, node) return True - case MTI.Producer_Identified_Active | MTI.Producer_Identified_Inactive | MTI.Producer_Identified_Unknown | MTI.Producer_Consumer_Event_Report : + case MTI.Producer_Identified_Active | MTI.Producer_Identified_Inactive | MTI.Producer_Identified_Unknown | MTI.Producer_Consumer_Event_Report : # noqa: E501 self.producedEventIndicated(message, node) return True - case MTI.Consumer_Identified_Active | MTI.Consumer_Identified_Inactive | MTI.Consumer_Identified_Unknown : + case MTI.Consumer_Identified_Active | MTI.Consumer_Identified_Inactive | MTI.Consumer_Identified_Unknown : # noqa: E501 self.consumedEventIndicated(message, node) return True case MTI.New_Node_Seen : @@ -61,66 +71,71 @@ def process(self, message, node) : # we ignore others return False return False - + def initializationComplete(self, message, node) : if self.checkSourceID(message, node) : # Send by us? node.state = Node.State.Initialized - # clear out PIP, SNIP caches - may have changed while node was offline + # clear out PIP, SNIP caches + # - may have changed while node was offline node.pipSet = set(()) node.snip = SNIP() - + def linkUpMessage(self, message, node) : # affects everybody node.state = Node.State.Uninitialized # don't clear out PIP, SNIP caches, they're probably still good - + def linkDownMessage(self, message, node) : # affects everybody node.state = Node.State.Uninitialized # don't clear out PIP, SNIP caches, they're probably still good - + def newNodeSeen(self, message, node) : # send pip and snip requests for info from the new node - pip = Message(MTI.Protocol_Support_Inquiry, self.linkLayer.localNodeID, node.id, []) + pip = Message(MTI.Protocol_Support_Inquiry, + self.linkLayer.localNodeID, node.id, []) self.linkLayer.sendMessage(pip) - # We request SNIP data on startup so that we can display node names. Can consider deferring this is it's a issue on big networks - snip = Message(MTI.Simple_Node_Ident_Info_Request, self.linkLayer.localNodeID, node.id, []) + # We request SNIP data on startup so that we can display node names. + # Can consider deferring this is it's a issue on big networks + snip = Message(MTI.Simple_Node_Ident_Info_Request, + self.linkLayer.localNodeID, node.id, []) self.linkLayer.sendMessage(snip) # we request produced and consumed event IDs - eventReq = Message(MTI.Identify_Events_Addressed, self.linkLayer.localNodeID, node.id, []) + eventReq = Message(MTI.Identify_Events_Addressed, + self.linkLayer.localNodeID, node.id, []) self.linkLayer.sendMessage(eventReq) - + def protocolSupportReply(self, message, node) : - if self.checkSourceID(message, node) : # sent by us? + if self.checkSourceID(message, node) : # sent by us? part0 = ((message.data[0]) << 24) if (len(message.data) > 0) else 0 part1 = ((message.data[1]) << 16) if (len(message.data) > 1) else 0 part2 = ((message.data[2]) << 8) if (len(message.data) > 2) else 0 part3 = ((message.data[3]) ) if (len(message.data) > 3) else 0 - content = part0|part1|part2|part3 + content = part0 | part1 | part2 | part3 node.pipSet = PIP.setContentsFromInt(content) - + def simpleNodeIdentInfoRequest(self, message, node) : - if self.checkDestID(message, node) : # sent by us? - overlapping SNIP activity is otherwise confusing + if self.checkDestID(message, node) : # sent by us? - overlapping SNIP activity is otherwise confusing # noqa: E501 # clear SNIP in the node to start accumulating node.snip = SNIP() def simpleNodeIdentInfoReply(self, message, node) : - if self.checkSourceID(message, node) : # sent by this node? - overlapping SNIP activity is otherwise confusing + if self.checkSourceID(message, node) : # sent by this node? - overlapping SNIP activity is otherwise confusing # noqa: E501 # accumulate data in the node if len(message.data) > 2 : node.snip.addData(message.data) node.snip.updateStringsFromSnipData() def producedEventIndicated(self, message, node) : - if self.checkSourceID(message, node) : # produced by this node? + if self.checkSourceID(message, node) : # produced by this node? # make an event id from the data eventID = EventID(message.data) # register it node.events.produces(eventID) - + def consumedEventIndicated(self, message, node) : - if self.checkSourceID(message, node) : # consumed by this node? + if self.checkSourceID(message, node) : # consumed by this node? # make an event id from the data eventID = EventID(message.data) # register it diff --git a/openlcb/remotenodestore.py b/openlcb/remotenodestore.py index 800830e..512550b 100644 --- a/openlcb/remotenodestore.py +++ b/openlcb/remotenodestore.py @@ -9,9 +9,10 @@ from openlcb.node import Node from openlcb.nodeid import NodeID + class RemoteNodeStore(NodeStore) : - ''' - Accumulates Nodes that it sees requested, unless they're already in a given local NodeStore + '''Accumulates Nodes that it sees requested + unless they're already in a given local NodeStore. ''' def __init__(self, localNodeID) : @@ -19,20 +20,22 @@ def __init__(self, localNodeID) : NodeStore.__init__(self) def description(self) : + '''Provide a more detailed string description ''' - Provide a more detailed string description - ''' - return "RemoteNodeStore w {}".format(self.nodes.count) + return "RemoteNodeStore w {}".format(self.nodes.count) def checkForNewNode(self, message) : - ''' - Return True if the message is to a new node, so that createNewRemoteNode should be called. + '''Check if the message is to a new node. + Returns: + bool: True if the message is to a new node, so that + createNewRemoteNode should be called. ''' node_id = message.source if node_id == self.localNodeID : # present in other store, skip return False - # NodeID(0) is a special case, used for e.g. linkUp, linkDown; don't store + # NodeID(0) is a special case, used for + # e.g. linkUp, linkDown; don't store if node_id == NodeID(0) : return False # make sure source node is in store if it needs to be @@ -58,10 +61,14 @@ def createNewRemoteNode(self, message) : processor.process(new_message, node) def processMessageFromLinkLayer(self, message) : - ''' - Process an incoming message across all the nodes in the remote node store. - Returns True is any of the nodes indicated a significant change. - - Parameter message: Incoming message to process + '''Process an incoming message + across all the nodes in the remote node store. + + Args: + message (Message): Incoming message to process + + Returns: + bool: True is any of the nodes indicated a significant change. ''' publish = False diff --git a/openlcb/snip.py b/openlcb/snip.py index 33c49c5..538e74d 100644 --- a/openlcb/snip.py +++ b/openlcb/snip.py @@ -1,13 +1,13 @@ import logging + class SNIP: - ''' - Holds the Simple Node Information Protocol values or blank strings. + '''Holds the Simple Node Information Protocol values or blank strings. - Provides support for loading via short or long messages. A SNIP is write-once; - when the underlying connection resets, a new SNIP struct should be installed in - the node. + Provides support for loading via short or long messages. A SNIP is + write-once; when the underlying connection resets, a new SNIP struct should + be installed in the node. ''' def __init__(self, mfgName="", diff --git a/openlcb/tcplink/tcpsocket.py b/openlcb/tcplink/tcpsocket.py index e91edfc..7acc658 100644 --- a/openlcb/tcplink/tcpsocket.py +++ b/openlcb/tcplink/tcpsocket.py @@ -5,21 +5,22 @@ # https://docs.python.org/3/howto/sockets.html import socket + class TcpSocket: def __init__(self, sock=None): if sock is None: self.sock = socket.socket( - socket.AF_INET, socket.SOCK_STREAM) + socket.AF_INET, + socket.SOCK_STREAM, + ) else: self.sock = sock def connect(self, host, port): self.sock.connect((host, port)) - def send(self, data): - ''' - Send a single message, provided as an [int] + '''Send a single message, provided as an [int] ''' msg = bytes(data) totalsent = 0 @@ -31,8 +32,10 @@ def send(self, data): def receive(self): '''Receive one or more bytes and return as an [int] - Blocks until at least one byte is received, but may return more. + + Returns: + list(int): one or more bytes, converted to a list of ints. ''' chunk = self.sock.recv(128) if chunk == b'': diff --git a/tests/test_canlink.py b/tests/test_canlink.py index 8ddcc80..dd5bac9 100644 --- a/tests/test_canlink.py +++ b/tests/test_canlink.py @@ -595,27 +595,27 @@ def testAmdAmrSequence(self): # MARK: - Data size handling def testSegmentAddressedDataArray(self): - canLink = CanLink( NodeID("05.01.01.01.03.01")) + canLink = CanLink(NodeID("05.01.01.01.03.01")) # no data - self.assertEqual(canLink.segmentAddressedDataArray((0x123), []), [[0x1,0x23]]) + self.assertEqual(canLink.segmentAddressedDataArray((0x123), []), [[0x1,0x23]]) # noqa: E231,E501 # short data - self.assertEqual(canLink.segmentAddressedDataArray((0x123), [0x1, 0x2]), [[0x1,0x23, 0x01, 0x02]]) + self.assertEqual(canLink.segmentAddressedDataArray((0x123), [0x1, 0x2]), [[0x1,0x23, 0x01, 0x02]]) # noqa: E231,E501 # full first frame - self.assertEqual(canLink.segmentAddressedDataArray((0x123), [0x1, 0x2, 0x3, 0x4, 0x5, 0x6]), [[0x1,0x23, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6]]) + self.assertEqual(canLink.segmentAddressedDataArray((0x123), [0x1, 0x2, 0x3, 0x4, 0x5, 0x6]), [[0x1,0x23, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6]]) # noqa: E231,E501 # two frames needed - self.assertEqual(canLink.segmentAddressedDataArray((0x123), [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7]), [[0x11,0x23, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6], [0x21,0x23, 0x7]]) + self.assertEqual(canLink.segmentAddressedDataArray((0x123), [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7]), [[0x11,0x23, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6], [0x21,0x23, 0x7]]) # noqa: E231,E501 # two full frames needed - self.assertEqual(canLink.segmentAddressedDataArray((0x123), [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC]), - [[0x11,0x23, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6], [0x21,0x23, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC]]) + self.assertEqual(canLink.segmentAddressedDataArray((0x123), [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC]), # noqa: E501 + [[0x11,0x23, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6], [0x21,0x23, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC]]) # noqa: E231,E501 # three frames needed - self.assertEqual(canLink.segmentAddressedDataArray((0x123), [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE]), - [[0x11,0x23, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6], [0x31,0x23, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC], [0x21, 0x23, 0xD, 0xE]]) + self.assertEqual(canLink.segmentAddressedDataArray((0x123), [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE]), # noqa: E501 + [[0x11,0x23, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6], [0x31,0x23, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC], [0x21, 0x23, 0xD, 0xE]]) # noqa: E231,E501 def testSegmentDatagramDataArray(self): canLink = CanLink(NodeID("05.01.01.01.03.01")) @@ -624,24 +624,24 @@ def testSegmentDatagramDataArray(self): self.assertEqual(canLink.segmentDatagramDataArray([]), [[]]) # short data - self.assertEqual(canLink.segmentDatagramDataArray([0x1, 0x2]), [[0x01, 0x02]]) + self.assertEqual(canLink.segmentDatagramDataArray([0x1, 0x2]), [[0x01, 0x02]]) # noqa: E501 # partially full first frame - self.assertEqual(canLink.segmentDatagramDataArray([0x1, 0x2, 0x3, 0x4, 0x5, 0x6]), [[0x1, 0x2, 0x3, 0x4, 0x5, 0x6]]) + self.assertEqual(canLink.segmentDatagramDataArray([0x1, 0x2, 0x3, 0x4, 0x5, 0x6]), [[0x1, 0x2, 0x3, 0x4, 0x5, 0x6]]) # noqa: E501 # one full frame needed - self.assertEqual(canLink.segmentDatagramDataArray([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]), [[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]]) + self.assertEqual(canLink.segmentDatagramDataArray([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]), [[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]]) # noqa: E501 # two frames needed - self.assertEqual(canLink.segmentDatagramDataArray([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9]), [[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8], [0x9]]) + self.assertEqual(canLink.segmentDatagramDataArray([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9]), [[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8], [0x9]]) # noqa: E501 # two full frames needed - self.assertEqual(canLink.segmentDatagramDataArray([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10]), - [[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8], [0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10]]) + self.assertEqual(canLink.segmentDatagramDataArray([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10]), # noqa: E501 + [[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8], [0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10]]) # noqa: E501 # three frames needed - self.assertEqual(canLink.segmentDatagramDataArray([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10, 0x11]), - [[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8], [0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10], [0x11]]) + self.assertEqual(canLink.segmentDatagramDataArray([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10, 0x11]), # noqa: E501 + [[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8], [0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x10], [0x11]]) # noqa: E501 if __name__ == '__main__': diff --git a/tests/test_canphysicallayergridconnect.py b/tests/test_canphysicallayergridconnect.py index 418c880..1986b11 100644 --- a/tests/test_canphysicallayergridconnect.py +++ b/tests/test_canphysicallayergridconnect.py @@ -1,6 +1,8 @@ import unittest -from openlcb.canbus.canphysicallayergridconnect import CanPhysicalLayerGridConnect +from openlcb.canbus.canphysicallayergridconnect import ( + CanPhysicalLayerGridConnect, +) from openlcb.canbus.canframe import CanFrame from openlcb.nodeid import NodeID @@ -124,4 +126,3 @@ def testSequence(self): if __name__ == '__main__': unittest.main() - diff --git a/tests/test_datagramservice.py b/tests/test_datagramservice.py index 22d859b..0f99ad6 100644 --- a/tests/test_datagramservice.py +++ b/tests/test_datagramservice.py @@ -167,4 +167,3 @@ def testReceiveDatagramOK(self): if __name__ == '__main__': unittest.main() - diff --git a/tests/test_linklayer.py b/tests/test_linklayer.py index 04c7544..d2f0aea 100644 --- a/tests/test_linklayer.py +++ b/tests/test_linklayer.py @@ -16,9 +16,9 @@ def receiveListener(self, msg): self.received = True def testReceipt(self): - received = False - msg = Message(MTI.Initialization_Complete, NodeID(12), NodeID(21)) - receiver = self.receiveListener + self.received = False + msg = Message(MTI.Initialization_Complete, NodeID(12), NodeID(21)) + receiver = self.receiveListener layer = LinkLayer(NodeID(100)) layer.registerMessageReceivedListener(receiver) diff --git a/tests/test_localeventstore.py b/tests/test_localeventstore.py index e577260..5127bcb 100644 --- a/tests/test_localeventstore.py +++ b/tests/test_localeventstore.py @@ -4,21 +4,23 @@ from openlcb.eventid import EventID + class TestLocalEventStorelass(unittest.TestCase): def testConsumes(self) : store = LocalEventStore() - + store.consumes(EventID(2)) self.assertTrue(store.isConsumed(EventID(2))) self.assertFalse(store.isConsumed(EventID(3))) def testProduces(self) : store = LocalEventStore() - + store.produces(EventID(4)) self.assertTrue(store.isProduced(EventID(4))) self.assertFalse(store.isProduced(EventID(5))) - + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_localnodeprocessor.py b/tests/test_localnodeprocessor.py index 15ab4e8..6a2e5cc 100644 --- a/tests/test_localnodeprocessor.py +++ b/tests/test_localnodeprocessor.py @@ -32,8 +32,8 @@ def testLinkUp(self): self.assertEqual(self.node21.state, Node.State.Initialized) self.assertEqual(len(LinkMockLayer.sentMessages), 1) self.assertEqual(LinkMockLayer.sentMessages[0], - Message(MTI.Initialization_Complete, self.node21.id, None, - self.node21.id.toArray())) + Message(MTI.Initialization_Complete, self.node21.id, + None, self.node21.id.toArray())) # self.assertEqual(LinkMockLayer.sentMessages[1], # Message(MTI.Verify_NodeID_Number_Global, self.node21.id)) diff --git a/tests/test_memoryservice.py b/tests/test_memoryservice.py index 043e439..9f5778d 100644 --- a/tests/test_memoryservice.py +++ b/tests/test_memoryservice.py @@ -109,27 +109,29 @@ def testMultipleRead(self): # ^ only one memory request datagram sent # have to reply through DatagramService - self.dService.process(Message(MTI.Datagram_Received_OK, NodeID(123), NodeID(12))) - self.assertEqual(len(LinkMockLayer.sentMessages), 1) # memory request datagram sent - self.assertEqual(LinkMockLayer.sentMessages[0].data, [0x20, 0x41, 0,0,0,0, 64]) - self.assertEqual(len(self.returnedMemoryReadMemo), 0) # no memory read op returned + self.dService.process(Message(MTI.Datagram_Received_OK, NodeID(123), + NodeID(12))) + self.assertEqual(len(LinkMockLayer.sentMessages), 1) # memory request datagram sent + self.assertEqual(LinkMockLayer.sentMessages[0].data, [0x20, 0x41, 0,0,0,0, 64]) # noqa: E231,E501 + self.assertEqual(len(self.returnedMemoryReadMemo), 0) # no memory read op returned - self.dService.process(Message(MTI.Datagram, NodeID(123), NodeID(12), [0x20, 0x51, 0,0,0,0, 1,2,3,4])) - self.assertEqual(len(LinkMockLayer.sentMessages), 3) # read reply datagram reply sent and next datagram sent - self.assertEqual(len(self.returnedMemoryReadMemo), 1) # memory read returned + self.dService.process(Message(MTI.Datagram, NodeID(123), NodeID(12), [0x20, 0x51, 0,0,0,0, 1,2,3,4])) # noqa: E231,E501 + self.assertEqual(len(LinkMockLayer.sentMessages), 3) # read reply datagram reply sent and next datagram sent + self.assertEqual(len(self.returnedMemoryReadMemo), 1) # memory read returned # walk through 2nd datagram - self.dService.process(Message(MTI.Datagram_Received_OK, NodeID(123), NodeID(12))) - self.assertEqual(len(LinkMockLayer.sentMessages), 3) # memory request datagram sent - self.assertEqual(LinkMockLayer.sentMessages[2].data, [0x20, 0x41, 0,0,0,64, 32]) - self.assertEqual(len(self.returnedMemoryReadMemo), 1) # no memory read op returned + self.dService.process(Message(MTI.Datagram_Received_OK, NodeID(123), + NodeID(12))) + self.assertEqual(len(LinkMockLayer.sentMessages), 3) # memory request datagram sent + self.assertEqual(LinkMockLayer.sentMessages[2].data, [0x20, 0x41, 0,0,0,64, 32]) # noqa: E231,E501 + self.assertEqual(len(self.returnedMemoryReadMemo), 1) # no memory read op returned self.dService.process(Message(MTI.Datagram, NodeID(123), NodeID(12), [0x20, 0x51, 0, 0, 0, 64, 1, 2, 3, 4])) - self.assertEqual(len(LinkMockLayer.sentMessages), 5) # read reply datagram reply sent and next datagram sent - self.assertEqual(len(self.returnedMemoryReadMemo), 2) # memory read returned + self.assertEqual(len(LinkMockLayer.sentMessages), 5) # read reply datagram reply sent and next datagram sent + self.assertEqual(len(self.returnedMemoryReadMemo), 2) # memory read returned def testArrayToString(self): sut = self.mService.arrayToString([0x41, 0x42, 0x43, 0x44], 4) @@ -176,4 +178,3 @@ def testSpaceDecode(self): if __name__ == '__main__': unittest.main() - diff --git a/tests/test_remotenodeprocessor.py b/tests/test_remotenodeprocessor.py index d5a093d..f83e779 100644 --- a/tests/test_remotenodeprocessor.py +++ b/tests/test_remotenodeprocessor.py @@ -17,126 +17,142 @@ def setUp(self) : self.node21 = Node(NodeID(21)) self.processor = RemoteNodeProcessor(CanLink(NodeID(100))) - def testInitializationComplete(self) : # not related to node msg1 = Message(MTI.Initialization_Complete, NodeID(13), None) - self.assertEqual(self.node21.state, Node.State.Uninitialized, "node state starts uninitialized") + self.assertEqual(self.node21.state, Node.State.Uninitialized, + "node state starts uninitialized") self.processor.process(msg1, self.node21) - self.assertEqual(self.node21.state, Node.State.Uninitialized, "node state stays uninitialized") + self.assertEqual(self.node21.state, Node.State.Uninitialized, + "node state stays uninitialized") # send by node msg2 = Message(MTI.Initialization_Complete, NodeID(21), None) - self.assertEqual(self.node21.state, Node.State.Uninitialized, "node state starts uninitialized") + self.assertEqual(self.node21.state, Node.State.Uninitialized, + "node state starts uninitialized") self.processor.process(msg2, self.node21) - self.assertEqual(self.node21.state, Node.State.Initialized, "node state goes initialized") - - + self.assertEqual(self.node21.state, Node.State.Initialized, + "node state goes initialized") def testPipReplyFull(self) : - msg1 = Message(MTI.Protocol_Support_Reply, NodeID(12), NodeID(13), [0x10, 0x10, 0x00, 0x00]) + msg1 = Message(MTI.Protocol_Support_Reply, NodeID(12), NodeID(13), + [0x10, 0x10, 0x00, 0x00]) self.processor.process(msg1, self.node21) - self.assertEqual(self.node21.pipSet, set(()) ); + self.assertEqual(self.node21.pipSet, set(())) - msg2 = Message(MTI.Protocol_Support_Reply, NodeID(21), NodeID(12), [0x10, 0x10, 0x00, 0x00]) + msg2 = Message(MTI.Protocol_Support_Reply, NodeID(21), NodeID(12), + [0x10, 0x10, 0x00, 0x00]) self.processor.process(msg2, self.node21) - self.assertEqual(self.node21.pipSet, set([PIP.MEMORY_CONFIGURATION_PROTOCOL, PIP.SIMPLE_NODE_IDENTIFICATION_PROTOCOL])) - + self.assertEqual(self.node21.pipSet, + set([PIP.MEMORY_CONFIGURATION_PROTOCOL, + PIP.SIMPLE_NODE_IDENTIFICATION_PROTOCOL])) def testPipReply2(self) : - msg1 = Message(MTI.Protocol_Support_Reply, NodeID(12), NodeID(13), [0x10, 0x10]) + msg1 = Message(MTI.Protocol_Support_Reply, NodeID(12), NodeID(13), + [0x10, 0x10]) self.processor.process(msg1, self.node21) - self.assertEqual(self.node21.pipSet, set(()) ) + self.assertEqual(self.node21.pipSet, set(())) - msg2 = Message(MTI.Protocol_Support_Reply, NodeID(21), NodeID(12), [0x10, 0x10]) + msg2 = Message(MTI.Protocol_Support_Reply, NodeID(21), NodeID(12), + [0x10, 0x10]) self.processor.process(msg2, self.node21) - self.assertEqual(self.node21.pipSet, set([PIP.MEMORY_CONFIGURATION_PROTOCOL, PIP.SIMPLE_NODE_IDENTIFICATION_PROTOCOL])) - + self.assertEqual(self.node21.pipSet, + set([PIP.MEMORY_CONFIGURATION_PROTOCOL, + PIP.SIMPLE_NODE_IDENTIFICATION_PROTOCOL])) def testPipReplyEmpty(self) : msg = Message(MTI.Protocol_Support_Reply, NodeID(12), NodeID(13)) self.processor.process(msg, self.node21) self.assertEqual(self.node21.pipSet, set(())) - def testLinkDown(self) : self.node21.pipSet = set([PIP.EVENT_EXCHANGE_PROTOCOL]) self.node21.state = Node.State.Initialized msg = Message(MTI.Link_Layer_Down, NodeID(0), NodeID(0)) self.processor.process(msg, self.node21) - self.assertEqual(self.node21.pipSet, set([PIP.EVENT_EXCHANGE_PROTOCOL])) + self.assertEqual(self.node21.pipSet, + set([PIP.EVENT_EXCHANGE_PROTOCOL])) self.assertEqual(self.node21.state, Node.State.Uninitialized) - def testLinkUp(self) : self.node21.pipSet = set([PIP.EVENT_EXCHANGE_PROTOCOL]) self.node21.state = Node.State.Initialized msg = Message(MTI.Link_Layer_Up, NodeID(0), NodeID(0)) self.processor.process(msg, self.node21) - self.assertEqual(self.node21.pipSet, set([PIP.EVENT_EXCHANGE_PROTOCOL])) + self.assertEqual(self.node21.pipSet, + set([PIP.EVENT_EXCHANGE_PROTOCOL])) self.assertEqual(self.node21.state, Node.State.Uninitialized) - def testUndefinedType(self) : - msg = Message(MTI.Unknown, NodeID(12), NodeID(13)) # neither to nor from us + msg = Message(MTI.Unknown, NodeID(12), NodeID(13)) # neither to nor from us # nothing but logging happens on an unknown type self.processor.process(msg, self.node21) - - msg = Message(MTI.Unknown, NodeID(12), NodeID(21)) # to us + + msg = Message(MTI.Unknown, NodeID(12), NodeID(21)) # to us # nothing but logging happens on an unknown type self.processor.process(msg, self.node21) - def testSnipHandling(self) : self.node21.snip.manufacturerName = "name present" - + # message not to us - msg = Message(MTI.Simple_Node_Ident_Info_Request, NodeID(12), NodeID(13)) + msg = Message(MTI.Simple_Node_Ident_Info_Request, NodeID(12), + NodeID(13)) self.processor.process(msg, self.node21) - + # should not have cleared SNIP and cache self.assertEqual(self.node21.snip.manufacturerName, "name present") - + # message to us - msg = Message(MTI.Simple_Node_Ident_Info_Request, NodeID(12), NodeID(21)) + msg = Message(MTI.Simple_Node_Ident_Info_Request, NodeID(12), + NodeID(21)) self.processor.process(msg, self.node21) - + # should have cleared SNIP and cache self.assertEqual(self.node21.snip.manufacturerName, "") - - # add some data - msg = Message(MTI.Simple_Node_Ident_Info_Reply, NodeID(21), NodeID(12), [4,0x31,0x32,0,0,0]) + + # add some data + msg = Message(MTI.Simple_Node_Ident_Info_Reply, NodeID(21), NodeID(12), + [4, 0x31, 0x32, 0, 0, 0]) self.processor.process(msg, self.node21) self.assertEqual(self.node21.snip.manufacturerName, "12") - def testProducerIdentified(self) : self.node21.state = Node.State.Initialized - msg = Message(MTI.Producer_Identified_Active, self.node21.id, None, [1,2,3,4,5,6,7,8]) + msg = Message(MTI.Producer_Identified_Active, self.node21.id, None, + [1, 2, 3, 4, 5, 6, 7, 8]) self.processor.process(msg, self.node21) - self.assertTrue(self.node21.events.isProduced(EventID(0x01_02_03_04_05_06_07_08))) - + self.assertTrue(self.node21.events.isProduced( + EventID(0x01_02_03_04_05_06_07_08) + )) def testProducerIdentifiedDifferentNode(self) : self.node21.state = Node.State.Initialized - msg = Message(MTI.Producer_Identified_Active, NodeID(1), None, [1,2,3,4,5,6,7,8]) + msg = Message(MTI.Producer_Identified_Active, NodeID(1), None, + [1, 2, 3, 4, 5, 6, 7, 8]) self.processor.process(msg, self.node21) - self.assertFalse(self.node21.events.isProduced(EventID(0x01_02_03_04_05_06_07_08))) - + self.assertFalse(self.node21.events.isProduced( + EventID(0x01_02_03_04_05_06_07_08) + )) def testConsumerIdentified(self) : self.node21.state = Node.State.Initialized - msg = Message(MTI.Consumer_Identified_Active, self.node21.id, None, [1,2,3,4,5,6,7,8]) + msg = Message(MTI.Consumer_Identified_Active, self.node21.id, None, + [1, 2, 3, 4, 5, 6, 7, 8]) self.processor.process(msg, self.node21) - self.assertTrue(self.node21.events.isConsumed(EventID(0x01_02_03_04_05_06_07_08))) - + self.assertTrue(self.node21.events.isConsumed( + EventID(0x01_02_03_04_05_06_07_08) + )) def testConsumerIdentifiedDifferentNode(self) : self.node21.state = Node.State.Initialized - msg = Message(MTI.Consumer_Identified_Active, NodeID(1), None, [1,2,3,4,5,6,7,8]) + msg = Message(MTI.Consumer_Identified_Active, NodeID(1), None, + [1, 2, 3, 4, 5, 6, 7, 8]) self.processor.process(msg, self.node21) - self.assertFalse(self.node21.events.isConsumed(EventID(0x01_02_03_04_05_06_07_08))) + self.assertFalse(self.node21.events.isConsumed( + EventID(0x01_02_03_04_05_06_07_08) + )) def testNewNodeSeen(self) : self.node21.state = Node.State.Initialized diff --git a/tests/test_remotenodestore.py b/tests/test_remotenodestore.py index f62fe4b..27d13d7 100644 --- a/tests/test_remotenodestore.py +++ b/tests/test_remotenodestore.py @@ -4,31 +4,30 @@ from openlcb.nodeid import NodeID from openlcb.remotenodestore import RemoteNodeStore + class TestRemoteNodeStoreClass(unittest.TestCase) : def testSimpleLoadStore(self) : store = RemoteNodeStore(NodeID(1)) - + n12 = Node(NodeID(12)) - + store.store(n12) store.store(Node(NodeID(13))) - - self.assertEqual(store.lookup(NodeID(12)), n12, "store then lookup OK") + self.assertEqual(store.lookup(NodeID(12)), n12, "store then lookup OK") def testRequestCreates(self) : nodeStore = RemoteNodeStore(NodeID(1)) - + # try a load temp = nodeStore.lookup(NodeID(12)) - - self.assertEqual(temp, None, "lookup returns None if node not present") + self.assertEqual(temp, None, "lookup returns None if node not present") def testAccessThroughLoadStoreByID(self) : nodeStore = RemoteNodeStore(NodeID(1)) - + nid12 = NodeID(12) nid13 = NodeID(13) @@ -37,39 +36,44 @@ def testAccessThroughLoadStoreByID(self) : nodeStore.store(n12) nodeStore.store(n13) - + # test ability to modify state n12.state = Node.State.Initialized - self.assertEqual(n12.state, Node.State.Initialized, "local modification OK") - self.assertEqual(nodeStore.lookup(nid12).state, Node.State.Initialized, "original in store modified") - + self.assertEqual(n12.state, Node.State.Initialized, + "local modification OK") + self.assertEqual(nodeStore.lookup(nid12).state, Node.State.Initialized, + "original in store modified") + # lookup non-existing node returns None - self.assertEqual(nodeStore.lookup(NodeID(21)), None, "None on no match in store") - + self.assertEqual(nodeStore.lookup(NodeID(21)), None, + "None on no match in store") + temp = nodeStore.lookup(nid13) temp.state = Node.State.Uninitialized nodeStore.store(temp) - self.assertEqual(nodeStore.lookup(nid13).state, Node.State.Uninitialized, - "original in store modified by replacement") - - + self.assertEqual(nodeStore.lookup(nid13).state, + Node.State.Uninitialized, + "original in store modified by replacement") def testALocalStoreVeto(self) : nid12 = NodeID(12) n12 = Node(nid12) - + nid13 = NodeID(13) - + store = RemoteNodeStore(nid13) store.store(n12) - + # lookup non-existing node doesn't create it if in local store - self.assertEqual(store.lookup(nid13), None, "don't create if in local store") - - def testCustomStringConvertible(self) : # existence test, don't check content which can change + self.assertEqual(store.lookup(nid13), None, + "don't create if in local store") + + def testCustomStringConvertible(self) : + # existence test, don't check content which can change store = RemoteNodeStore(NodeID(13)) store.description + if __name__ == '__main__': unittest.main() diff --git a/tests/test_tcplink.py b/tests/test_tcplink.py index 9666978..92f0ccf 100644 --- a/tests/test_tcplink.py +++ b/tests/test_tcplink.py @@ -33,7 +33,7 @@ def testLinkUpSequence(self): linkLayer = TcpLink(NodeID(100)) linkLayer.registerMessageReceivedListener(messageLayer.receiveMessage) linkLayer.linkPhysicalLayer(tcpLayer.send) - + linkLayer.linkUp() self.assertEqual(len(tcpLayer.receivedText), 0) @@ -46,7 +46,7 @@ def testLinkRestartSequence(self): linkLayer = TcpLink(NodeID(100)) linkLayer.registerMessageReceivedListener(messageLayer.receiveMessage) linkLayer.linkPhysicalLayer(tcpLayer.send) - + linkLayer.linkRestarted() self.assertEqual(len(tcpLayer.receivedText), 0) @@ -59,12 +59,12 @@ def testLinkDownSequence(self): linkLayer = TcpLink(NodeID(100)) linkLayer.registerMessageReceivedListener(messageLayer.receiveMessage) linkLayer.linkPhysicalLayer(tcpLayer.send) - + linkLayer.linkDown() self.assertEqual(len(tcpLayer.receivedText), 0) self.assertEqual(len(messageLayer.receivedMessages), 1) - + def testOneMessageOnePartOneClump(self) : messageLayer = MessageMockLayer() tcpLayer = TcpMockLayer() @@ -72,20 +72,21 @@ def testOneMessageOnePartOneClump(self) : linkLayer = TcpLink(NodeID(100)) linkLayer.registerMessageReceivedListener(messageLayer.receiveMessage) linkLayer.linkPhysicalLayer(tcpLayer.send) - - messageText = [0x80, 0x00, # full message - 0x00, 0x00, 20, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time - 0x04, 0x90, # MTI: VerifyNode - 0x00, 0x00, 0x00, 0x00, 0x03, 0x21 # source NodeID - ] - linkLayer.receiveListener(messageText) - + + messageText = [0x80, 0x00, # full message + 0x00, 0x00, 20, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time + 0x04, 0x90, # MTI: VerifyNode + 0x00, 0x00, 0x00, 0x00, 0x03, 0x21 # source NodeID + ] + linkLayer.receiveListener(messageText) + self.assertEqual(len(tcpLayer.receivedText), 0) self.assertEqual(len(messageLayer.receivedMessages), 1) - self.assertEqual(messageLayer.receivedMessages[0].source, NodeID(0x321)) - + self.assertEqual(messageLayer.receivedMessages[0].source, + NodeID(0x321)) + def testOneMessageOnePartTwoClumps(self) : messageLayer = MessageMockLayer() tcpLayer = TcpMockLayer() @@ -93,23 +94,24 @@ def testOneMessageOnePartTwoClumps(self) : linkLayer = TcpLink(NodeID(100)) linkLayer.registerMessageReceivedListener(messageLayer.receiveMessage) linkLayer.linkPhysicalLayer(tcpLayer.send) - - messageText = [0x80, 0x00, # full message - 0x00, 0x00, 20, + + messageText = [0x80, 0x00, # full message + 0x00, 0x00, 20, ] - linkLayer.receiveListener(messageText) - - messageText = [0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time - 0x04, 0x90, # MTI: VerifyNode - 0x00, 0x00, 0x00, 0x00, 0x03, 0x21 # source NodeID - ] - linkLayer.receiveListener(messageText) - + linkLayer.receiveListener(messageText) + + messageText = [0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time + 0x04, 0x90, # MTI: VerifyNode + 0x00, 0x00, 0x00, 0x00, 0x03, 0x21 # source NodeID + ] + linkLayer.receiveListener(messageText) + self.assertEqual(len(tcpLayer.receivedText), 0) self.assertEqual(len(messageLayer.receivedMessages), 1) - self.assertEqual(messageLayer.receivedMessages[0].source, NodeID(0x321)) - + self.assertEqual(messageLayer.receivedMessages[0].source, + NodeID(0x321)) + def testOneMessageOnePartThreeClumps(self) : messageLayer = MessageMockLayer() tcpLayer = TcpMockLayer() @@ -117,25 +119,26 @@ def testOneMessageOnePartThreeClumps(self) : linkLayer = TcpLink(NodeID(100)) linkLayer.registerMessageReceivedListener(messageLayer.receiveMessage) linkLayer.linkPhysicalLayer(tcpLayer.send) - - messageText = [0x80, 0x00, # full message - 0x00, 0x00, 20, + + messageText = [0x80, 0x00, # full message + 0x00, 0x00, 20, ] - linkLayer.receiveListener(messageText) + linkLayer.receiveListener(messageText) - messageText = [0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time + messageText = [0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time ] - - linkLayer.receiveListener(messageText) - messageText = [0x04, 0x90, # MTI: VerifyNode - 0x00, 0x00, 0x00, 0x00, 0x03, 0x21 # source NodeID - ] - linkLayer.receiveListener(messageText) - + + linkLayer.receiveListener(messageText) + messageText = [0x04, 0x90, # MTI: VerifyNode + 0x00, 0x00, 0x00, 0x00, 0x03, 0x21 # source NodeID + ] + linkLayer.receiveListener(messageText) + self.assertEqual(len(tcpLayer.receivedText), 0) self.assertEqual(len(messageLayer.receivedMessages), 1) - self.assertEqual(messageLayer.receivedMessages[0].source, NodeID(0x321)) + self.assertEqual(messageLayer.receivedMessages[0].source, + NodeID(0x321)) def testTwoMessageOnePartTwoClumps(self) : messageLayer = MessageMockLayer() @@ -144,30 +147,32 @@ def testTwoMessageOnePartTwoClumps(self) : linkLayer = TcpLink(NodeID(100)) linkLayer.registerMessageReceivedListener(messageLayer.receiveMessage) linkLayer.linkPhysicalLayer(tcpLayer.send) - - messageText = [0x80, 0x00, # full message - 0x00, 0x00, 20, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time - 0x04, 0x90, # MTI: VerifyNode - 0x00, 0x00, 0x00, 0x00, 0x03, 0x21 # source NodeID - ] - linkLayer.receiveListener(messageText) - - messageText = [0x80, 0x00, # full message - 0x00, 0x00, 20, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time - 0x04, 0x90, # MTI: VerifyNode - 0x00, 0x00, 0x00, 0x00, 0x04, 0x56 # source NodeID - ] - linkLayer.receiveListener(messageText) - + + messageText = [0x80, 0x00, # full message + 0x00, 0x00, 20, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time + 0x04, 0x90, # MTI: VerifyNode + 0x00, 0x00, 0x00, 0x00, 0x03, 0x21 # source NodeID + ] + linkLayer.receiveListener(messageText) + + messageText = [0x80, 0x00, # full message + 0x00, 0x00, 20, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time + 0x04, 0x90, # MTI: VerifyNode + 0x00, 0x00, 0x00, 0x00, 0x04, 0x56 # source NodeID + ] + linkLayer.receiveListener(messageText) + self.assertEqual(len(tcpLayer.receivedText), 0) self.assertEqual(len(messageLayer.receivedMessages), 2) - self.assertEqual(messageLayer.receivedMessages[0].source, NodeID(0x321)) - self.assertEqual(messageLayer.receivedMessages[1].source, NodeID(0x456)) - + self.assertEqual(messageLayer.receivedMessages[0].source, + NodeID(0x321)) + self.assertEqual(messageLayer.receivedMessages[1].source, + NodeID(0x456)) + def testOneMessageTwoPartsOneClump(self) : messageLayer = MessageMockLayer() tcpLayer = TcpMockLayer() @@ -175,26 +180,27 @@ def testOneMessageTwoPartsOneClump(self) : linkLayer = TcpLink(NodeID(100)) linkLayer.registerMessageReceivedListener(messageLayer.receiveMessage) linkLayer.linkPhysicalLayer(tcpLayer.send) - - messageText = [0x80, 0x40, # part 1 - 0x00, 0x00, 13, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time - 0x04, # first half MTI - - 0x80, 0x80, # part 2 - 0x00, 0x00, 19, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time - 0x90, # second half MTI - 0x00, 0x00, 0x00, 0x00, 0x03, 0x21 # source NodeID - ] - linkLayer.receiveListener(messageText) - + + messageText = [0x80, 0x40, # part 1 + 0x00, 0x00, 13, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time + 0x04, # first half MTI + + 0x80, 0x80, # part 2 + 0x00, 0x00, 19, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time + 0x90, # second half MTI + 0x00, 0x00, 0x00, 0x00, 0x03, 0x21 # source NodeID + ] + linkLayer.receiveListener(messageText) + self.assertEqual(len(tcpLayer.receivedText), 0) self.assertEqual(len(messageLayer.receivedMessages), 1) - self.assertEqual(messageLayer.receivedMessages[0].source, NodeID(0x321)) - + self.assertEqual(messageLayer.receivedMessages[0].source, + NodeID(0x321)) + def testOneMessageThreePartsOneClump(self) : messageLayer = MessageMockLayer() tcpLayer = TcpMockLayer() @@ -202,32 +208,33 @@ def testOneMessageThreePartsOneClump(self) : linkLayer = TcpLink(NodeID(100)) linkLayer.registerMessageReceivedListener(messageLayer.receiveMessage) linkLayer.linkPhysicalLayer(tcpLayer.send) - - messageText = [0x80, 0x40, # part 1 - 0x00, 0x00, 13, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time - 0x04, # first half MTI - - 0x80, 0xC0, # part 2 - 0x00, 0x00, 13, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time - 0x90, # second half MTI - # no data - - 0x80, 0x80, # part 3 - 0x00, 0x00, 18, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time - 0x00, 0x00, 0x00, 0x00, 0x03, 0x21 # source NodeID - ] - linkLayer.receiveListener(messageText) - + + messageText = [0x80, 0x40, # part 1 + 0x00, 0x00, 13, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time + 0x04, # first half MTI + + 0x80, 0xC0, # part 2 + 0x00, 0x00, 13, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time + 0x90, # second half MTI + # no data + + 0x80, 0x80, # part 3 + 0x00, 0x00, 18, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, # source node ID + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, # time + 0x00, 0x00, 0x00, 0x00, 0x03, 0x21 # source NodeID + ] + linkLayer.receiveListener(messageText) + self.assertEqual(len(tcpLayer.receivedText), 0) self.assertEqual(len(messageLayer.receivedMessages), 1) - self.assertEqual(messageLayer.receivedMessages[0].source, NodeID(0x321)) - + self.assertEqual(messageLayer.receivedMessages[0].source, + NodeID(0x321)) + def testSendGlobalMessage(self) : messageLayer = MessageMockLayer() tcpLayer = TcpMockLayer() @@ -235,23 +242,24 @@ def testSendGlobalMessage(self) : linkLayer = TcpLink(NodeID(100)) linkLayer.registerMessageReceivedListener(messageLayer.receiveMessage) linkLayer.linkPhysicalLayer(tcpLayer.send) - - message = Message(MTI.Verify_NodeID_Number_Global, NodeID(0x123), None, NodeID(0x321).toArray()) + + message = Message(MTI.Verify_NodeID_Number_Global, NodeID(0x123), None, + NodeID(0x321).toArray()) linkLayer.sendMessage(message) - + self.assertEqual(len(tcpLayer.receivedText), 1) - self.assertEqual(tcpLayer.receivedText[0][0:11],[ # can't check time - 0x80, 0x00, - 0x00, 0x00, 26, - 0x00, 0x00, 0x00, 0x00, 0x00, 100, - ]) - self.assertEqual(tcpLayer.receivedText[0][17:],[ # can't check time - 0x04, 0x90, # MTI - 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, - 0x00, 0x00, 0x00, 0x00, 0x03, 0x21, - ]) + self.assertEqual(tcpLayer.receivedText[0][0:11], [ # can't check time + 0x80, 0x00, + 0x00, 0x00, 26, + 0x00, 0x00, 0x00, 0x00, 0x00, 100, + ]) + self.assertEqual(tcpLayer.receivedText[0][17:], [ # can't check time + 0x04, 0x90, # MTI + 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x21, + ]) self.assertEqual(len(messageLayer.receivedMessages), 0) - + def testSendAddressedMessage(self) : messageLayer = MessageMockLayer() tcpLayer = TcpMockLayer() @@ -259,24 +267,25 @@ def testSendAddressedMessage(self) : linkLayer = TcpLink(NodeID(100)) linkLayer.registerMessageReceivedListener(messageLayer.receiveMessage) linkLayer.linkPhysicalLayer(tcpLayer.send) - - message = Message(MTI.Verify_NodeID_Number_Addressed, NodeID(0x123), NodeID(0x321), NodeID(0x321).toArray()) + + message = Message(MTI.Verify_NodeID_Number_Addressed, NodeID(0x123), + NodeID(0x321), NodeID(0x321).toArray()) linkLayer.sendMessage(message) - + self.assertEqual(len(tcpLayer.receivedText), 1) - self.assertEqual(tcpLayer.receivedText[0][0:11],[ # can't check time - 0x80, 0x00, - 0x00, 0x00, 32, - 0x00, 0x00, 0x00, 0x00, 0x00, 100, - ]) - self.assertEqual(tcpLayer.receivedText[0][17:],[ # can't check time - 0x04, 0x88, # MTI - 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, - 0x00, 0x00, 0x00, 0x00, 0x03, 0x21, - 0x00, 0x00, 0x00, 0x00, 0x03, 0x21, - ]) + self.assertEqual(tcpLayer.receivedText[0][0:11], [ # can't check time + 0x80, 0x00, + 0x00, 0x00, 32, + 0x00, 0x00, 0x00, 0x00, 0x00, 100, + ]) + self.assertEqual(tcpLayer.receivedText[0][17:], [ # can't check time + 0x04, 0x88, # MTI + 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x21, + 0x00, 0x00, 0x00, 0x00, 0x03, 0x21, + ]) self.assertEqual(len(messageLayer.receivedMessages), 0) - + if __name__ == '__main__': unittest.main() From cff3d9aaa92dbbfa2ce2268e32be90cf20ea3d99 Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:27:52 -0400 Subject: [PATCH 11/16] Use PEP8 more. Add a comment regarding an error. Ignore some long lines that are inconvenient to split (noqa: E501). --- example_cdi_access.py | 10 +++++----- openlcb/mti.py | 15 ++++++++++----- openlcb/tcplink/tcplink.py | 1 + tests/test_memoryservice.py | 16 ++++++++-------- tests/test_remotenodeprocessor.py | 2 +- 5 files changed, 25 insertions(+), 19 deletions(-) diff --git a/example_cdi_access.py b/example_cdi_access.py index f7b890c..a159a44 100644 --- a/example_cdi_access.py +++ b/example_cdi_access.py @@ -175,7 +175,7 @@ def memoryReadFail(memo): # in a row, we buffer up the characters until the `endElement` # call is invoked to indicate the text is complete -import xml.sax +import xml.sax # noqa: E402 class MyHandler(xml.sax.handler.ContentHandler): @@ -184,13 +184,13 @@ def __init__(self): self._charBuffer = [] def startElement(self, name, attrs): - print ("Start: ", name) + print("Start: ", name) if attrs is not None and attrs : - print (" Atributes: ", attrs.getNames()) + print(" Atributes: ", attrs.getNames()) def endElement(self, name): - print (name, "cpntent:", self._flushCharBuffer()) - print ("End: ", name) + print(name, "cpntent:", self._flushCharBuffer()) + print("End: ", name) pass def _flushCharBuffer(self): diff --git a/openlcb/mti.py b/openlcb/mti.py index 5565a0c..2899d4c 100644 --- a/openlcb/mti.py +++ b/openlcb/mti.py @@ -57,12 +57,17 @@ class MTI(Enum): New_Node_Seen = 0x2048 # alias resolution found new node; marked addressed (0x8 bit) # noqa: E501 - def priority(self): return (self.value & 0x0C00) >> 10 + def priority(self): + return (self.value & 0x0C00) >> 10 - def addressPresent(self): return (self.value & 0x0008) != 0 + def addressPresent(self): + return (self.value & 0x0008) != 0 - def eventIDPresent(self): return (self.value & 0x0004) != 0 + def eventIDPresent(self): + return (self.value & 0x0004) != 0 - def simpleProtocol(self): return (self.value & 0x0010) != 0 + def simpleProtocol(self): + return (self.value & 0x0010) != 0 - def isGlobal(self): return (self.value & 0x0008) == 0 + def isGlobal(self): + return (self.value & 0x0008) == 0 diff --git a/openlcb/tcplink/tcplink.py b/openlcb/tcplink/tcplink.py index 56b8c02..0dec6cc 100644 --- a/openlcb/tcplink/tcplink.py +++ b/openlcb/tcplink/tcplink.py @@ -146,6 +146,7 @@ def forwardMessage(self, messageBytes, gatewayNodeID) : # not sure why gatewayN data = messageBytes[8:] if mti.addressPresent() : destNodeID = NodeID(messagePart[8:13]) + # FIXME: ^ messagePart is undefined. Slice data or messageBytes? data = messageBytes[14:] # and finally create the message message = Message(mti, sourceNodeID, destNodeID, data) diff --git a/tests/test_memoryservice.py b/tests/test_memoryservice.py index 9f5778d..3fc7b67 100644 --- a/tests/test_memoryservice.py +++ b/tests/test_memoryservice.py @@ -111,27 +111,27 @@ def testMultipleRead(self): # have to reply through DatagramService self.dService.process(Message(MTI.Datagram_Received_OK, NodeID(123), NodeID(12))) - self.assertEqual(len(LinkMockLayer.sentMessages), 1) # memory request datagram sent + self.assertEqual(len(LinkMockLayer.sentMessages), 1) # memory request datagram sent # noqa: E501 self.assertEqual(LinkMockLayer.sentMessages[0].data, [0x20, 0x41, 0,0,0,0, 64]) # noqa: E231,E501 - self.assertEqual(len(self.returnedMemoryReadMemo), 0) # no memory read op returned + self.assertEqual(len(self.returnedMemoryReadMemo), 0) # no memory read op returned # noqa: E501 self.dService.process(Message(MTI.Datagram, NodeID(123), NodeID(12), [0x20, 0x51, 0,0,0,0, 1,2,3,4])) # noqa: E231,E501 - self.assertEqual(len(LinkMockLayer.sentMessages), 3) # read reply datagram reply sent and next datagram sent - self.assertEqual(len(self.returnedMemoryReadMemo), 1) # memory read returned + self.assertEqual(len(LinkMockLayer.sentMessages), 3) # read reply datagram reply sent and next datagram sent # noqa: E501 + self.assertEqual(len(self.returnedMemoryReadMemo), 1) # memory read returned # noqa: E501 # walk through 2nd datagram self.dService.process(Message(MTI.Datagram_Received_OK, NodeID(123), NodeID(12))) - self.assertEqual(len(LinkMockLayer.sentMessages), 3) # memory request datagram sent + self.assertEqual(len(LinkMockLayer.sentMessages), 3) # memory request datagram sent # noqa: E501 self.assertEqual(LinkMockLayer.sentMessages[2].data, [0x20, 0x41, 0,0,0,64, 32]) # noqa: E231,E501 - self.assertEqual(len(self.returnedMemoryReadMemo), 1) # no memory read op returned + self.assertEqual(len(self.returnedMemoryReadMemo), 1) # no memory read op returned # noqa: E501 self.dService.process(Message(MTI.Datagram, NodeID(123), NodeID(12), [0x20, 0x51, 0, 0, 0, 64, 1, 2, 3, 4])) - self.assertEqual(len(LinkMockLayer.sentMessages), 5) # read reply datagram reply sent and next datagram sent - self.assertEqual(len(self.returnedMemoryReadMemo), 2) # memory read returned + self.assertEqual(len(LinkMockLayer.sentMessages), 5) # read reply datagram reply sent and next datagram sent # noqa: E501 + self.assertEqual(len(self.returnedMemoryReadMemo), 2) # memory read returned # noqa: E501 def testArrayToString(self): sut = self.mService.arrayToString([0x41, 0x42, 0x43, 0x44], 4) diff --git a/tests/test_remotenodeprocessor.py b/tests/test_remotenodeprocessor.py index f83e779..d08f514 100644 --- a/tests/test_remotenodeprocessor.py +++ b/tests/test_remotenodeprocessor.py @@ -84,7 +84,7 @@ def testLinkUp(self) : self.assertEqual(self.node21.state, Node.State.Uninitialized) def testUndefinedType(self) : - msg = Message(MTI.Unknown, NodeID(12), NodeID(13)) # neither to nor from us + msg = Message(MTI.Unknown, NodeID(12), NodeID(13)) # neither to nor from us # noqa: E501 # nothing but logging happens on an unknown type self.processor.process(msg, self.node21) From 44250f89c99401322637e51265d993d79bc31658 Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:23:44 -0400 Subject: [PATCH 12/16] Fix a bug from a renamed variable (bug introduced in cf4a375 which tried to fix W0622 redefined builtin "input"). Clarify related docstrings. --- openlcb/pip.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openlcb/pip.py b/openlcb/pip.py index bd66f44..3437739 100644 --- a/openlcb/pip.py +++ b/openlcb/pip.py @@ -57,15 +57,16 @@ def setContentsFromInt(bitmask): """Get a set of contents from a single numeric bitmask Args: - bitmask (int): A single number that is the sum of 1 or more + bitmask (int): A single number that is the sum of any number of protocol bits. Returns: - set (PIP): The set of protocol bits derived from the bitmask. + set (PIP): The set of protocol bits (bitmasks where 1 bit is on in + each) derived from the bitmask. """ retVal = [] for val in PIP.list(): - if (val.value & input != 0): + if (val.value & bitmask != 0): retVal.append(val) return set(retVal) From 84a977a243a9ac92ee003ea75c556ca14f9bc13d Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Fri, 5 Apr 2024 12:53:25 -0400 Subject: [PATCH 13/16] Change the rewrap limit for the typical case (comments, where length<=72 is preferred in PEP8) and set the editor's builtin GUI-only (visual) wrapping to the atypical use case (code, where length <=79 is required by PEP8; atypical since usually wrapped manually). --- python-openlcb.code-workspace | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python-openlcb.code-workspace b/python-openlcb.code-workspace index 137ec61..5bd62e0 100644 --- a/python-openlcb.code-workspace +++ b/python-openlcb.code-workspace @@ -5,13 +5,14 @@ } ], "settings": { - "rewrap.wrappingColumn": 79, + "rewrap.wrappingColumn": 72, "autoDocstring.docstringFormat": "google", "flake8.args": [ "--ignore=E203,E226,E701,E202,E222,E221,W503,E241" ], "ruff.lint.args": [ "--ignore=E701" - ] + ], + "editor.wordWrapColumn": 79 } } \ No newline at end of file From 1013d3f2f44f725205012237f633b1165992d685 Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:56:37 -0400 Subject: [PATCH 14/16] Make examples configurable. Add machine-specific settings and dumps to .gitignore. --- .gitignore | 4 + example_cdi_access.py | 54 ++--- example_datagram_transfer.py | 39 +--- example_frame_interface.py | 39 +--- example_memory_transfer.py | 48 ++--- example_message_interface.py | 46 ++-- example_node_implementation.py | 50 ++--- example_remote_nodes.py | 77 +++---- example_string_interface.py | 40 +--- example_string_serial_interface.py | 37 +--- example_tcp_message_interface.py | 51 ++--- examples_settings.py | 331 +++++++++++++++++++++++++++++ 12 files changed, 468 insertions(+), 348 deletions(-) create mode 100644 examples_settings.py diff --git a/.gitignore b/.gitignore index 7db8fcd..78b22e2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ *localoverrides.py *tempCDI.xml +/*.bad_json.txt +/settings.json +/.vscode/settings.json +# ^ /.vscode/settings.json is ignored since it may have python.defaultInterpreterPath with differs depending on the specific machine. Recommended: place that setting in there ("PythonOlcbNode Folder" tab in VSCode settings) \ No newline at end of file diff --git a/example_cdi_access.py b/example_cdi_access.py index a159a44..c70dd59 100644 --- a/example_cdi_access.py +++ b/example_cdi_access.py @@ -1,5 +1,6 @@ ''' -Demo of using the memory service to read the CDI from memory, then an example of parsing +Demo of using the memory service to read the CDI from memory, then an +example of parsing Usage: python3 example_memory_transfer.py [host|host:port] @@ -27,47 +28,24 @@ ) # specify connection information -host = "192.168.16.212" -port = 12021 -localNodeID = "05.01.01.01.03.01" -# farNodeID = "09.00.99.03.00.35" -farNodeID = "02.01.57.00.04.9C" +# region moved to settings +# host = "192.168.16.212" +# port = 12021 +# localNodeID = "05.01.01.01.03.01" +# # farNodeID = "09.00.99.03.00.35" +# farNodeID = "02.01.57.00.04.9C" +# endregion moved to settings # region same code as other examples - - -def usage(): - print(__doc__, file=sys.stderr) - +from examples_settings import Settings +settings = Settings() if __name__ == "__main__": - # global host # only necessary if this is moved to a main/other function - import sys - if len(sys.argv) == 2: - host = sys.argv[1] - parts = host.split(":") - if len(parts) == 2: - host = parts[0] - try: - port = int(parts[1]) - except ValueError: - usage() - print("Error: Port {} is not an integer.".format(parts[1]), - file=sys.stderr) - sys.exit(1) - elif len(parts) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - elif len(sys.argv) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - + settings.load_cli_args(docstring=__doc__) # endregion same code as other examples s = TcpSocket() -s.connect(host, port) +s.connect(settings['host'], settings['port']) # print("RR, SR are raw socket interface receive and send;" @@ -106,7 +84,7 @@ def printDatagram(memo): canPhysicalLayerGridConnect = CanPhysicalLayerGridConnect(sendToSocket) canPhysicalLayerGridConnect.registerFrameReceivedListener(printFrame) -canLink = CanLink(NodeID(localNodeID)) +canLink = CanLink(NodeID(settings['localNodeID'])) canLink.linkPhysicalLayer(canPhysicalLayerGridConnect) canLink.registerMessageReceivedListener(printMessage) @@ -232,8 +210,8 @@ def memoryRead(): time.sleep(1) # read 64 bytes from the CDI space starting at address zero - memMemo = MemoryReadMemo(NodeID(farNodeID), 64, 0xFF, 0, memoryReadFail, - memoryReadSuccess) + memMemo = MemoryReadMemo(NodeID(settings['farNodeID']), 64, 0xFF, 0, + memoryReadFail, memoryReadSuccess) memoryService.requestMemoryRead(memMemo) diff --git a/example_datagram_transfer.py b/example_datagram_transfer.py index 157b749..c11612e 100644 --- a/example_datagram_transfer.py +++ b/example_datagram_transfer.py @@ -23,47 +23,24 @@ ) # specify connection information -host = "192.168.16.212" -port = 12021 +# region replaced by settings +# host = "192.168.16.212" +# port = 12021 +# endregion replaced by settings # region same code as other examples - - -def usage(): - print(__doc__, file=sys.stderr) - +from examples_settings import Settings +settings = Settings() if __name__ == "__main__": - # global host # only necessary if this is moved to a main/other function - import sys - if len(sys.argv) == 2: - host = sys.argv[1] - parts = host.split(":") - if len(parts) == 2: - host = parts[0] - try: - port = int(parts[1]) - except ValueError: - usage() - print("Error: Port {} is not an integer.".format(parts[1]), - file=sys.stderr) - sys.exit(1) - elif len(parts) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - elif len(sys.argv) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - + settings.load_cli_args(docstring=__doc__) # endregion same code as other examples localNodeID = "05.01.01.01.03.01" farNodeID = "09.00.99.03.00.35" s = TcpSocket() -s.connect(host, port) +s.connect(settings['host'], settings['port']) print("RR, SR are raw socket interface receive and send;" " RL, SL are link interface; RM, SM are message interface") diff --git a/example_frame_interface.py b/example_frame_interface.py index 4870a9d..991ae5e 100644 --- a/example_frame_interface.py +++ b/example_frame_interface.py @@ -19,44 +19,21 @@ from openlcb.canbus.controlframe import ControlFrame # specify connection information -host = "192.168.16.212" -port = 12021 +# region replaced by settings +# host = "192.168.16.212" +# port = 12021 +# endregion replaced by settings # region same code as other examples - - -def usage(): - print(__doc__, file=sys.stderr) - +from examples_settings import Settings +settings = Settings() if __name__ == "__main__": - # global host # only necessary if this is moved to a main/other function - import sys - if len(sys.argv) == 2: - host = sys.argv[1] - parts = host.split(":") - if len(parts) == 2: - host = parts[0] - try: - port = int(parts[1]) - except ValueError: - usage() - print("Error: Port {} is not an integer.".format(parts[1]), - file=sys.stderr) - sys.exit(1) - elif len(parts) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - elif len(sys.argv) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - + settings.load_cli_args(docstring=__doc__) # endregion same code as other examples s = TcpSocket() -s.connect(host, port) +s.connect(settings['host'], settings['port']) print("RR, SR are raw socket interface receive and send;" " RL, SL are link (frame) interface") diff --git a/example_memory_transfer.py b/example_memory_transfer.py index 4d12d96..4d001ec 100644 --- a/example_memory_transfer.py +++ b/example_memory_transfer.py @@ -30,46 +30,23 @@ ) # specify connection information -host = "192.168.16.212" -port = 12021 -localNodeID = "05.01.01.01.03.01" -farNodeID = "09.00.99.03.00.35" +# region replaced by settings +# host = "192.168.16.212" +# port = 12021 +# localNodeID = "05.01.01.01.03.01" +# farNodeID = "09.00.99.03.00.35" +# endregion replaced by settings # region same code as other examples - - -def usage(): - print(__doc__, file=sys.stderr) - +from examples_settings import Settings +settings = Settings() if __name__ == "__main__": - # global host # only necessary if this is moved to a main/other function - import sys - if len(sys.argv) == 2: - host = sys.argv[1] - parts = host.split(":") - if len(parts) == 2: - host = parts[0] - try: - port = int(parts[1]) - except ValueError: - usage() - print("Error: Port {} is not an integer.".format(parts[1]), - file=sys.stderr) - sys.exit(1) - elif len(parts) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - elif len(sys.argv) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - + settings.load_cli_args(docstring=__doc__) # endregion same code as other examples s = TcpSocket() -s.connect(host, port) +s.connect(settings['host'], settings['port']) print("RR, SR are raw socket interface receive and send;" " RL, SL are link interface; RM, SM are message interface") @@ -92,7 +69,7 @@ def printMessage(message): print("RM: {} from {}".format(message, message.source)) -canLink = CanLink(NodeID(localNodeID)) +canLink = CanLink(NodeID(settings['localNodeID'])) canLink.linkPhysicalLayer(canPhysicalLayerGridConnect) canLink.registerMessageReceivedListener(printMessage) @@ -149,7 +126,8 @@ def memoryRead(): time.sleep(1) # read 64 bytes from the CDI space starting at address zero - memMemo = MemoryReadMemo(NodeID(farNodeID), 64, 0xFF, 0, memoryReadFail, + memMemo = MemoryReadMemo(NodeID(settings['farNodeID']), + 64, 0xFF, 0, memoryReadFail, memoryReadSuccess) memoryService.requestMemoryRead(memMemo) diff --git a/example_message_interface.py b/example_message_interface.py index 96f723e..163007d 100644 --- a/example_message_interface.py +++ b/example_message_interface.py @@ -22,45 +22,22 @@ from openlcb.mti import MTI # specify connection information -host = "192.168.16.212" -port = 12021 -localNodeID = "05.01.01.01.03.01" +# region replaced by settings +# host = "192.168.16.212" +# port = 12021 +# localNodeID = "05.01.01.01.03.01" +# endregion replaced by settings # region same code as other examples - - -def usage(): - print(__doc__, file=sys.stderr) - +from examples_settings import Settings +settings = Settings() if __name__ == "__main__": - # global host # only necessary if this is moved to a main/other function - import sys - if len(sys.argv) == 2: - host = sys.argv[1] - parts = host.split(":") - if len(parts) == 2: - host = parts[0] - try: - port = int(parts[1]) - except ValueError: - usage() - print("Error: Port {} is not an integer.".format(parts[1]), - file=sys.stderr) - sys.exit(1) - elif len(parts) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - elif len(sys.argv) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - + settings.load_cli_args(docstring=__doc__) # endregion same code as other examples s = TcpSocket() -s.connect(host, port) +s.connect(settings['host'], settings['port']) print("RR, SR are raw socket interface receive and send; RL," " SL are link interface; RM, SM are message interface") @@ -83,7 +60,7 @@ def printMessage(msg): print("RM: {} from {}".format(msg, msg.source)) -canLink = CanLink(NodeID(localNodeID)) +canLink = CanLink(NodeID(settings['localNodeID'])) canLink.linkPhysicalLayer(canPhysicalLayerGridConnect) canLink.registerMessageReceivedListener(printMessage) @@ -94,7 +71,8 @@ def printMessage(msg): canPhysicalLayerGridConnect.physicalLayerUp() # send an VerifyNodes message to provoke response -message = Message(MTI.Verify_NodeID_Number_Global, NodeID(localNodeID), None) +message = Message(MTI.Verify_NodeID_Number_Global, + NodeID(settings['localNodeID']), None) print("SM: {}".format(message)) canLink.sendMessage(message) diff --git a/example_node_implementation.py b/example_node_implementation.py index c5e04a1..245cd24 100644 --- a/example_node_implementation.py +++ b/example_node_implementation.py @@ -27,46 +27,23 @@ from openlcb.node import Node # specify connection information -host = "192.168.16.212" -port = 12021 -localNodeID = "05.01.01.01.03.01" -farNodeID = "09.00.99.03.00.35" +# region moved to settings +# host = "192.168.16.212" +# port = 12021 +# localNodeID = "05.01.01.01.03.01" +# farNodeID = "09.00.99.03.00.35" +# endregion moved to settings # region same code as other examples - - -def usage(): - print(__doc__, file=sys.stderr) - +from examples_settings import Settings +settings = Settings() if __name__ == "__main__": - # global host # only necessary if this is moved to a main/other function - import sys - if len(sys.argv) == 2: - host = sys.argv[1] - parts = host.split(":") - if len(parts) == 2: - host = parts[0] - try: - port = int(parts[1]) - except ValueError: - usage() - print("Error: Port {} is not an integer.".format(parts[1]), - file=sys.stderr) - sys.exit(1) - elif len(parts) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - elif len(sys.argv) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - + settings.load_cli_args(docstring=__doc__) # endregion same code as other examples s = TcpSocket() -s.connect(host, port) +s.connect(settings['host'], settings['port']) print("RR, SR are raw socket interface receive and send;" " RL, SL are link interface; RM, SM are message interface") @@ -89,7 +66,7 @@ def printMessage(message): print("RM: {} from {}".format(message, message.source)) -canLink = CanLink(NodeID(localNodeID)) +canLink = CanLink(NodeID(settings['localNodeID'])) canLink.linkPhysicalLayer(canPhysicalLayerGridConnect) canLink.registerMessageReceivedListener(printMessage) @@ -129,7 +106,7 @@ def memoryReadFail(memo): # This is a very minimal node, which just takes part in the low-level common # protocols localNode = Node( - NodeID(localNodeID), + NodeID(settings['localNodeID']), SNIP("PythonOlcbNode", "example_node_implementation", "0.1", "0.2", "User Name Here", "User Description Here"), set([PIP.SIMPLE_NODE_IDENTIFICATION_PROTOCOL, PIP.DATAGRAM_PROTOCOL]) @@ -161,7 +138,8 @@ def displayOtherNodeIds(message) : canPhysicalLayerGridConnect.physicalLayerUp() # request that nodes identify themselves so that we can print their node IDs -message = Message(MTI.Verify_NodeID_Number_Global, NodeID(localNodeID), None) +message = Message(MTI.Verify_NodeID_Number_Global, + NodeID(settings['localNodeID']), None) canLink.sendMessage(message) # process resulting activity diff --git a/example_remote_nodes.py b/example_remote_nodes.py index 545cbf3..5e53230 100644 --- a/example_remote_nodes.py +++ b/example_remote_nodes.py @@ -33,61 +33,39 @@ from queue import Empty # specify default connection information -host = "192.168.16.212" -port = 12021 -localNodeID = "05.01.01.01.03.01" -trace = False -timeout = 0.5 +# region replaced by settings +# host = "192.168.16.212" +# port = 12021 +# localNodeID = "05.01.01.01.03.01" +# trace = False +# timeout = 0.5 +# endregion replaced by settings # region same code as other examples - - -def usage(): - print(__doc__, file=sys.stderr) - +from examples_settings import Settings +settings = Settings() if __name__ == "__main__": - # global host # only necessary if this is moved to a main/other function - import sys - if len(sys.argv) == 2: - host = sys.argv[1] - parts = host.split(":") - if len(parts) == 2: - host = parts[0] - try: - port = int(parts[1]) - except ValueError: - usage() - print("Error: Port {} is not an integer.".format(parts[1]), - file=sys.stderr) - sys.exit(1) - elif len(parts) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - elif len(sys.argv) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - + settings.load_cli_args(docstring=__doc__) # endregion same code as other examples + s = TcpSocket() -s.connect(host, port) +s.connect(settings['host'], settings['port']) -if trace : +if settings['trace'] : print("RR, SR are raw socket interface receive and send;" " RL, SL are link (frame) interface") def sendToSocket(string) : - if trace : print(" SR: "+string.strip()) + if settings['trace'] : print(" SR: "+string.strip()) s.send(string) def receiveFrame(frame) : - if trace: print("RL: "+str(frame)) + if settings['trace']: print("RL: "+str(frame)) canPhysicalLayerGridConnect = CanPhysicalLayerGridConnect(sendToSocket) @@ -95,11 +73,11 @@ def receiveFrame(frame) : def printMessage(msg): - if trace: print("RM: {} from {}".format(msg, msg.source)) + if settings['trace']: print("RM: {} from {}".format(msg, msg.source)) readQueue.put(msg) -canLink = CanLink(NodeID(localNodeID)) +canLink = CanLink(NodeID(settings['localNodeID'])) canLink.linkPhysicalLayer(canPhysicalLayerGridConnect) canLink.registerMessageReceivedListener(printMessage) @@ -107,7 +85,7 @@ def printMessage(msg): # This is a very minimal node, which just takes part in the low-level common # protocols localNode = Node( - NodeID(localNodeID), + NodeID(settings['localNodeID']), SNIP("PythonOlcbNode", "example_node_implementation", "0.1", "0.2", "User Name Here", "User Description Here"), set([PIP.SIMPLE_NODE_IDENTIFICATION_PROTOCOL, PIP.DATAGRAM_PROTOCOL]) @@ -117,7 +95,7 @@ def printMessage(msg): canLink.registerMessageReceivedListener(localNodeProcessor.process) # arrange for remote nodes to be tracked -remoteNodeStore = RemoteNodeStore(NodeID(localNodeID)) +remoteNodeStore = RemoteNodeStore(NodeID(settings['localNodeID'])) remoteNodeProcessor = RemoteNodeProcessor(canLink) remoteNodeStore.processors = [remoteNodeProcessor] canLink.registerMessageReceivedListener( @@ -131,11 +109,11 @@ def printMessage(msg): def receiveLoop() : """put the read on a separate thread""" # bring the CAN level up - if trace : print(" SL : link up") + if settings['trace'] : print(" SL : link up") canPhysicalLayerGridConnect.physicalLayerUp() while True: input = s.receive() - if trace : print(" RR: "+input.strip()) + if settings['trace'] : print(" RR: "+input.strip()) # pass to link processor canPhysicalLayerGridConnect.receiveString(input) @@ -181,22 +159,23 @@ def result(arg1, arg2=None, arg3=None, result=True) : # pull the received messages while True : try : - received = readQueue.get(True, timeout) - if trace : print("received: ", received) + received = readQueue.get(True, settings['timeout']) + if settings['trace'] : print("received: ", received) except Empty: break # send an VerifyNodes message to provoke response print("\nSend Verify NodeID Number Global\n") -message = Message(MTI.Verify_NodeID_Number_Global, NodeID(localNodeID), None) -if trace : print("SM: {}".format(message)) +message = Message(MTI.Verify_NodeID_Number_Global, + NodeID(settings['localNodeID']), None) +if settings['trace'] : print("SM: {}".format(message)) canLink.sendMessage(message) # pull the received messages while True : try : - received = readQueue.get(True, timeout) - if trace : print("received: ", received) + received = readQueue.get(True, settings['timeout']) + if settings['trace'] : print("received: ", received) except Empty: break diff --git a/example_string_interface.py b/example_string_interface.py index 3ea34d2..047e552 100644 --- a/example_string_interface.py +++ b/example_string_interface.py @@ -14,44 +14,22 @@ from openlcb.canbus.tcpsocket import TcpSocket # specify connection information -host = "192.168.16.212" -port = 12021 +# region replaced by settings +# host = "192.168.16.212" +# port = 12021 +# endregion replaced by settings # region same code as other examples - - -def usage(): - print(__doc__, file=sys.stderr) - +from examples_settings import Settings +settings = Settings() if __name__ == "__main__": - # global host # only necessary if this is moved to a main/other function - import sys - if len(sys.argv) == 2: - host = sys.argv[1] - parts = host.split(":") - if len(parts) == 2: - host = parts[0] - try: - port = int(parts[1]) - except ValueError: - usage() - print("Error: Port {} is not an integer.".format(parts[1]), - file=sys.stderr) - sys.exit(1) - elif len(parts) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - elif len(sys.argv) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - + settings.load_cli_args(docstring=__doc__) # endregion same code as other examples + s = TcpSocket() -s.connect(host, port) +s.connect(settings['host'], settings['port']) ####################### diff --git a/example_string_serial_interface.py b/example_string_serial_interface.py index 0205fa0..b2d2b6d 100644 --- a/example_string_serial_interface.py +++ b/example_string_serial_interface.py @@ -14,43 +14,20 @@ from openlcb.canbus.seriallink import SerialLink # specify connection information -device = "/dev/cu.usbmodemCC570001B1" +# region replaced by settings +# device = "/dev/cu.usbmodemCC570001B1" +# endregion replaced by settings # region same code as other examples - - -def usage(): - print(__doc__, file=sys.stderr) - +from examples_settings import Settings +settings = Settings() if __name__ == "__main__": - # global host # only necessary if this is moved to a main/other function - import sys - if len(sys.argv) == 2: - host = sys.argv[1] - parts = host.split(":") - if len(parts) == 2: - host = parts[0] - try: - port = int(parts[1]) - except ValueError: - usage() - print("Error: Port {} is not an integer.".format(parts[1]), - file=sys.stderr) - sys.exit(1) - elif len(parts) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - elif len(sys.argv) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - + settings.load_cli_args(docstring=__doc__) # endregion same code as other examples s = SerialLink() -s.connect(device) +s.connect(settings['device']) ####################### diff --git a/example_tcp_message_interface.py b/example_tcp_message_interface.py index 72624fc..a2322fa 100644 --- a/example_tcp_message_interface.py +++ b/example_tcp_message_interface.py @@ -19,51 +19,35 @@ from openlcb.mti import MTI # specify connection information -host = "localhost" -port = 12022 -localNodeID = "05.01.01.01.03.01" +# region moved to settings +# host = "localhost" +# port = 12022 +# localNodeID = "05.01.01.01.03.01" +# endregion moved to settings # region same code as other examples - - -def usage(): - print(__doc__, file=sys.stderr) - +from examples_settings import Settings +settings = Settings() if __name__ == "__main__": - # global host # only necessary if this is moved to a main/other function - import sys - if len(sys.argv) == 2: - host = sys.argv[1] - parts = host.split(":") - if len(parts) == 2: - host = parts[0] - try: - port = int(parts[1]) - except ValueError: - usage() - print("Error: Port {} is not an integer.".format(parts[1]), - file=sys.stderr) - sys.exit(1) - elif len(parts) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - elif len(sys.argv) > 2: - usage() - print("Error: blank, address or address:port format was expected.") - sys.exit(1) - + settings.load_cli_args(docstring=__doc__) # endregion same code as other examples s = TcpSocket() -s.connect(host, port) +print("Using settings:") +print(settings.dumps()) +s.connect(settings['host'], settings['port']) print("RR, SR are raw socket interface receive and send; " " RM, SM are message interface") def sendToSocket(data): + # if isinstance(data, list): + # raise TypeError( + # "Got {}({}) but expected str" + # .format(type(data).__name__, data) + # ) print(" SR: {}".format(data.strip())) s.send(data) @@ -83,7 +67,8 @@ def printMessage(msg): tcpLinklayer.linkUp() # send an VerifyNodes message to provoke response -message = Message(MTI.Verify_NodeID_Number_Global, NodeID(localNodeID), None) +message = Message(MTI.Verify_NodeID_Number_Global, + NodeID(settings['localNodeID']), None) print("SM: {}".format(message)) tcpLinklayer.sendMessage(message) diff --git a/examples_settings.py b/examples_settings.py new file mode 100644 index 0000000..543b670 --- /dev/null +++ b/examples_settings.py @@ -0,0 +1,331 @@ +"""Define options and an options loader that can be managed during runtime. + +Contributors: Poikilos +""" +import copy +import json +import os +import shutil +import sys + +CONFIGS_DIR = os.path.dirname(os.path.realpath(__file__)) + +DEFAULT_SETTINGS = { + "host": "192.168.16.212", + "port": 12021, + "localNodeID": "05.01.01.01.03.01", # Warning: *only for openlcb*: + # See localNodeID_comment in SETTINGS_COMMENTS. + "farNodeID": "02.01.57.00.04.9C", # Serialized if hardware: + # See farNodeID_comment in SETTINGS_COMMENTS. + "trace": False, # Such as for example_remote_nodes.py + "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 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). +} + +SETTINGS_COMMENTS = { + "localNodeID_comment": ( + "Warning: *only for openlcb*:" + " 05.01.01.01.03.01 is reserved by OpenLCB Python examples." + " Find or suggest your organization's range" + " at http://registry.openlcb.org/uniqueidranges" + " and serialize if producing hardware (See LCC Standard(s))." + ), + "farNodeID_comment": ( + "serialized hardware node: for example," + " 02.01.57.00.04.9C and 09.00.99.03.00.35 are bobjacobsen's" + ) +} + + +class Settings: + """Load runtime settings that can be shared by examples. + + Can load data from JSON (which is not committed to git, but) can be + generated during runtime before importing this since some example code may + run in the core of the module rather than in portable functions. + + Attributes: + _meta (dict): All of the settings, guaranteed to have the keys and + child keys if any of DEFAULT_SETTINGS as long as used properly (not + used directly by code outside of class). Defaults to + DEFAULT_SETTINGS. + settingtypes (dict): A list of setting types. This should at least be + set for float, int and bool, since in tkinter, IntVar is used for + bool in the case of Checkbutton, and StringVar is usually used for + numbers (if using Entry or Combobox). + + """ + SETTINGS_NAME = "settings.json" + SETTINGS_PATH = os.path.join(CONFIGS_DIR, SETTINGS_NAME) + _settingtypes = { + "trace": {"type": bool}, + "port": {"type": int}, + "timeout": {"type": (float, int)}, # first one is preferred if tuple + } + + def __init__(self): + # See class docstring for more info. + self._meta = None + self.known_program_keys = { + "example_remote_nodes": ["trace", "timeout"], + "example_string_serial_interface": ["device"], + } + self.settings_path = Settings.SETTINGS_PATH + self.load(self.settings_path, required=False) + + def __getitem__(self, key): + """The dunder method to allow brackets to get a value""" + if key not in self._meta: + raise KeyError( + "Error: either {} is not a valid setting," + " _meta was manipulated outside of the class," + " or the key should be added to DEFAULT_SETTINGS" + " to guarantee it exists." + "".format(key) + ) + return self._meta.get(key) + + def __setitem__(self, key, value): + """The dunder method to allow brackets to set a value""" + if key is None: + raise KeyError('None is not a valid setting name.') + if key not in self._meta: + print("Warning: {} is not the name of a known setting." + " To ensure it is the correct name and type," + " use an existing one or add it to DEFAULT_SETTINGS.", + file=sys.stderr) + old_value = self._meta.get(key) + old_type = type(old_value) + if old_value is None: + # In case the value was set to None in a previous call to this, + # use the default to determine the type: + old_type = type(DEFAULT_SETTINGS.get(key)) + if (value is not None) and (old_type is not None): + if not isinstance(value, type(old_value)): + raise TypeError( + 'Expected a(n) {} for {} but got {}({})' + ''.format(type(old_value), key, type(value).__name__, + value) + ) + self._meta[key] = value + + def __iter__(self): + """The dunder method to allow `key in settings` as a boolean + check, or after `for` to iterate. Works along with __next__ to + implement using an instance of this class as an iterator. + """ + self._keys = list(self._meta.keys()) + self._iterate_i = 0 + return self + + def __next__(self): + """The dunder method to allow `key in settings` as a boolean + check, or after `for` to iterate. Works along with __iter__ to + implement using an instance of this class as an iterator. + """ + if self._iterate_i >= len(self._keys): + raise StopIteration + key = self._keys[self._iterate_i] + self._iterate_i += 1 + return key + + def keys(self): + """Return the keys iterator from the settings dictionary. + + Returns: + iterator(str): All settings (each value is the name of a + setting). + """ + return self._meta.keys() + + def dumps(self, indent=2, sort_keys=True): + """Show all settings. + For args documentation, see json.dumps. + + Returns: + str: All settings, in JSON format. + """ + return json.dumps(self._meta, indent=indent, sort_keys=sort_keys) + + def get_types(self, key): + """Get the type or tuple of types allowed for a setting. + + Typically, set result to the return then you can do: + if result and not isinstance(var, result): # then depending on + the scenario, cast # (via `get_preferred_type(key)(var)`) or # + show a warning/error. See note under "Returns". + + Args: + key (str): The name of the setting. + + Returns: + Union(type,tuple(type)): A type or types, or None if not + defined (typically set the setting to a str value in + that case). NOTE: isinstance accepts a type or a tuple + of types, so the return of this method is designed to + match its usage (but only use return if it is not None). + """ + info = self._settingtypes.get(key) + if not info: + return None + return info.get("type") + + def get_preferred_type(self, key): + """Get the type for casting. + + This can be used alongside get_types, but always use + get_preferred_type to cast, since get_types may return a tuple + of types. + + Args: + key (str): The name of the setting. + + Returns: + type: A type to use. For example, set _type to the return + then do _type(value) (potentially, a casting function + could also be returned here, so only use the return + in that way. For isinstance, use get_types instead). + """ + _type = self.get_types(key) + if not _type: + return None + if isinstance(_type, tuple): + return _type[0] # The first entry is preferred. + return _type # There is only one entry, so it is preferred. + + def getDefault(self, key): + """Get default value of a setting. + + Args: + key (str): The name of the setting. + + Returns: + Misc: A value of whatever type is in DEFAULT_SETTINGS. + """ + if key not in DEFAULT_SETTINGS: + print("Warning: {} is not in DEFAULT_SETTINGS." + " You should probably use a known setting or a value for {}" + " should be added to DEFAULT_SETTINGS".format(key, key), + file=sys.stderr) + return DEFAULT_SETTINGS.get(key) + + def load(self, settings_path=None, required=False): + """Load the settings file. + + Args: + settings_path (str): Settings json path structured like + DEFAULT_SETTINGS. Defaults to Settings.SETTINGS_PATH. + required (bool): Whether to require the file exists and + raise an error if not. Ok to be False since: whether file is + present or not (and if False), this method places any missing + defaults into self._meta. Defaults to False. + + Raises: + FileNotFoundError: The settings file is not present and required is + True. + """ + if not settings_path: + settings_path = Settings.SETTINGS_PATH + if os.path.isfile(settings_path): + try: + with open(settings_path, 'r') as stream: + self._meta = json.load(stream) + except json.decoder.JSONDecodeError: + self._meta = copy.deepcopy(DEFAULT_SETTINGS) + no_ext, dot_ext = os.path.splitext(settings_path) + bad_path = no_ext+".bad_json.txt" + i = 1 + while os.path.isfile(bad_path): + # Find an unused filename for the backup. + i += 1 + bad_path = no_ext+"."+str(i)+".bad_json.txt" + print("Error: {} was not proper JSON. Backing up to {}" + " and loading defaults." + "".format(settings_path, bad_path), + file=sys.stderr) + shutil.move(settings_path, bad_path) + # ^ must be outside of "with" statement to move file + raise + elif required: + # self._meta = copy.deepcopy(DEFAULT_SETTINGS) + raise FileNotFoundError(settings_path) + if not self._meta: + # settings_path neither found nor required so load defaults + self._meta = copy.deepcopy(DEFAULT_SETTINGS) + + # self.settings_path = settings_path + for key, value in DEFAULT_SETTINGS.items(): + if key not in self._meta: + # If the saved settings file has a missing entry, add it. + self._meta[key] = copy.deepcopy(value) + if isinstance(value, dict): + # If the saved settings file has a missing child entry, add it. + for child_key, child in value.items(): + if child_key not in self._meta[key]: + self._meta[key][child_key] = copy.deepcopy(child) + + def save(self, settings_path=None): + """Save the settings. + + Args: + settings_path (str, optional): Path for saving. Defaults to + self.settings_path. + """ + if not settings_path: + settings_path = self.settings_path + self._meta.update(SETTINGS_COMMENTS) # include latest warnings in json + with open(settings_path, 'w') as stream: + json.dump(self._meta, stream, indent=1, sort_keys=True) + + def usage(self): + if not self.caller_documentation: + return + print(self.caller_documentation, file=sys.stderr) + + def load_cli_args(self, docstring=None): + """Load command-line interface arguments. + + Returns: + int: 0 if ok, otherwise failed (error was shown on stderr). + """ + self.caller_documentation = docstring + if len(sys.argv) == 2: + self['host'] = sys.argv[1] + parts = self['host'].split(":") + if len(parts) == 2: + self['host'] = parts[0] + try: + self['port'] = int(parts[1]) + except ValueError: + self.usage() + print("Error: Port {} is not an integer.".format(parts[1]), + file=sys.stderr) + return 1 + elif len(parts) > 2: + self.usage() + print("Error: blank, address or address:port format expected.", + file=sys.stderr) + return 1 + elif len(sys.argv) > 2: + self.usage() + print("Error: blank, address or address:port format expected" + "but got too many arguments: {}".format(sys.argv[1:]), + # 1: to skip name of file + file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + print(__doc__, file=sys.stderr) + print("Error: This file is a module." + " See actual example files which may use it like:", + file=sys.stderr) + print("from examples_settings import Settings", file=sys.stderr) + print("settings = Settings() # loads {} or defaults" + "".format(Settings.SETTINGS_NAME), file=sys.stderr) From 4f183a00ecb6f45893e1fcd17eca7d2118e0ef0e Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:15:21 -0400 Subject: [PATCH 15/16] Save json if it doesn't exist (Exposes settings to the user). --- examples_settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples_settings.py b/examples_settings.py index 543b670..0974e90 100644 --- a/examples_settings.py +++ b/examples_settings.py @@ -268,6 +268,10 @@ def load(self, settings_path=None, required=False): for child_key, child in value.items(): if child_key not in self._meta[key]: self._meta[key][child_key] = copy.deepcopy(child) + if not os.path.isfile(settings_path): + # Expose the settings to the user (even if not using GUI) + # by creating the json file if it doesn't exist: + self.save(settings_path) def save(self, settings_path=None): """Save the settings. From d2c68223a621665cd0806e6a25223455fbf31fcc Mon Sep 17 00:00:00 2001 From: Poikilos <7557867+Poikilos@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:28:47 -0400 Subject: [PATCH 16/16] Add a GUI that will specify settings for examples. --- examples_gui.py | 563 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 563 insertions(+) create mode 100644 examples_gui.py diff --git a/examples_gui.py b/examples_gui.py new file mode 100644 index 0000000..3ace9b5 --- /dev/null +++ b/examples_gui.py @@ -0,0 +1,563 @@ +""" +Examples GUI + +This file is part of the PythonOlcbNode project +(). + +Contributors: Poikilos + +Purpose: Provide an easy way to enter settings for examples and run them. +- tkinter is used since it is included in Python (except in Debian-based + distros, which require the python3-tk package due to Debian requirements + for GUI components to be packaged separately). +""" +import json +import os +import subprocess +import sys +import tkinter as tk +from tkinter import ttk +from collections import OrderedDict + +from examples_settings import Settings + +zeroconf_enabled = False +try: + from zeroconf import ServiceBrowser, ServiceListener, Zeroconf + zeroconf_enabled = True +except ImportError: + class Zeroconf: + """Placeholder for when zeroconf is *not* present""" + pass + + class ServiceListener: + """Placeholder for when zeroconf is *not* present""" + pass + + class ServiceBrowser: + """Placeholder for when zeroconf is *not* present""" + pass + + +class MyListener(ServiceListener): + pass + + +class DataField(): + """Store various widgets and data associated with a single data field. + Attributes: + label (Label): A label associated with (usually left of) the field. + var (Union[StringVar,IntVar]): Makes value accessible in a uniform way + (self.var.get()) regardless of widget class. + widget (Misc): The widget used to enter data (value is stored in var). + button (Button): optional command button. + tooltip (Label): An extra label associated with (usually below) the + field. + """ + def __init__(self): + self.label = None + self.var = None + self.widget = None + self.button = None + self.tooltip = None + + def get(self): + return self.var.get() + + def set(self, value): + self.var.set(value) + + +class MainForm(ttk.Frame): + """The interface to choose device(s) for examples. + + The program is organized into fields. Each field contains a label, entry + widget, and potentially a tooltip Label and command button. + - The entry widget for each field may be a ttk.Entry, ttk.Combobox, or + potentially another ttk widget subclass. + - Each field has a key. Only keys in self.settings are directly used + as settings. + + Attributes: + w1 (Union[Tk,Frame]): The Tk (first and only Window typically). It is + set automatically to self.winfo_toplevel() by gui method. + parent (Union[Tk,Frame]): Tk (same as self.w1 in that case) or tk.Frame + instance, whichever contains self. + fields (list[DataField]): A list of settings. + example_buttons (OrderedDict[Button]): The example module name is the + 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 + for now just run the file in another Python instance (See + run_example method). + + Args: + parent (Union[Tk,Frame,0]): The Tk (first and only Window typically) or + tk.Frame, either way containing self. 0 to make a new Tk. The + tk instance is always stored in self.w1 regardless of parent. + """ + + def __init__(self, parent): + self.zeroconf = None + self.listener = None + self.browser = None + self.errors = [] + try: + self.settings = Settings() + except json.decoder.JSONDecodeError as ex: + self.errors.append( + "Error: {} not loaded! {}".format(Settings.SETTINGS_NAME, ex) + ) + # Try again (load defaults), since Settings is expected to + # have backed up & moved the bad JSON file: + self.settings = Settings() + self.detected_services = OrderedDict() + self.fields = OrderedDict() + self.proc = None + self.gui(parent) + self.w1.after(1, self.on_form_loaded) # must go after gui + self.example_modules = OrderedDict() + self.example_buttons = OrderedDict() + if zeroconf_enabled: + self.zeroconf = Zeroconf() + self.listener = MyListener() + self.listener.update_service = self.update_service + self.listener.remove_service = self.remove_service + self.listener.add_service = self.add_service + + def on_form_loaded(self): + self.load_settings() + self.load_examples() + count = self.show_next_error() + if not count: + self.set_status( + "Welcome! Select an example. Run also saves settings." + ) + + def show_next_error(self): + if not self.errors: + return 0 + error = self.errors.pop(0) + if not error: + return 0 + self.set_status(error) + return 1 + + def remove_examples(self): + for module_name, button in self.example_buttons.items(): + button.grid_forget() + self.row -= 1 + self.example_buttons.clear() + self.example_modules.clear() + + def load_examples(self): + self.remove_examples() + repo_dir = os.path.dirname(os.path.realpath(__file__)) + self.example_var = tk.IntVar() # Shared by *all* in radio group. + # ^ The value refers to an entry in examples: + self.examples = [] + for sub in sorted(os.listdir(repo_dir)): + if not sub.startswith("example_"): + continue + if not sub.endswith(".py"): + continue + sub_path = os.path.join(repo_dir, sub) + name, _ = os.path.splitext(sub) # name, dot+extension + self.example_modules[name] = sub_path + button = ttk.Radiobutton( + self, + text=name, + variable=self.example_var, + value=len(self.examples), + # command=lambda x=name: self.run_example(module_name=x), + # x=name is necessary for early binding, otherwise all + # lambdas will have the *last* value in the loop. + ) + self.examples.append(name) + button.grid(row=self.row, column=1) + self.example_buttons[name] = button + self.row += 1 + self.run_button = ttk.Button( + self, + text="Run", + command=self.run_example, + # command=lambda x=name: self.run_example(module_name=x), + # x=name is necessary for early binding, otherwise all + # lambdas will have the *last* value in the loop. + ) + self.run_button.grid(row=self.row, column=1) + self.row += 1 + + def run_example(self, module_name=None): + """Run the selected example. + + Args: + module_name (str, optional): The module name (file without + extension) of the example. Defaults to selected + Radiobutton. + """ + if not module_name: + # for name, radiobutton in self.example_buttons.items(): + index = self.example_var.get() + if index is None: + self.set_status("Select an example first.") + return + module_name = self.examples[index] + + self.set_status("") + node_ids = ( + self.fields['localNodeID'].get(), + self.fields['farNodeID'].get(), + ) + for node_id in node_ids: + if (":" in node_id) or ("." not in node_id): + self.set_status("Error: expected dot-separated ID") + return + self.save_settings() + module_path = self.example_modules[module_name] + args = (sys.executable, module_path) + self.set_status("Running {} (see console for results)..." + "".format(module_name)) + self.proc = subprocess.Popen( + args, + shell=True, + # close_fds=True, close file descriptors >= 3 before running + # stdin=None, stdout=None, stderr=None, + ) + + def load_settings(self): + # import json + # print(json.dumps(self.settings._meta, indent=1, sort_keys=True)) + + # print("[gui] self.settings['localNodeID']={}" + # .format(self.settings['localNodeID'])) + for key, var in self.fields.items(): + if key not in self.settings: + # The field must not be a setting. Don't try to load + # (Avoid KeyError). + continue + self.fields[key].set(self.settings[key]) + # print("[gui] self.fields['localNodeID']={}" + # .format(self.fields['localNodeID'].get())) + + def save_settings(self): + for key, field in self.fields.items(): + if key not in self.settings: + # Skip runtime GUI data fields such as + # self.fields['service_name'] that aren't directly used as + # settings. + print("{} is not in settings.".format(key)) + continue + print("{} is in settings.".format(key)) + value = field.get() + _types = self.settings.get_types(key) + if _types: + if not isinstance(value, _types): + _type = self.settings.get_preferred_type(key) + # ^ Get the preferred type in case multiple are allowed + # (usually float or int in that case) + value = _type(value) + self.settings[key] = value + self.settings.save() + + def gui(self, parent): + print("Using {}".format(self.settings.settings_path)) + # import json + # print(json.dumps(self.settings._meta, indent=1, sort_keys=True)) + self.parent = parent + ttk.Frame.__init__(self, self.parent) + self.row_count = 0 + self.column_count = 0 + self.tooltip_column = 0 + self.tooltip_columnspan = 3 + self.grid_args = { + 'sticky': tk.NSEW, # N is top ("north"), W is left, etc. + # 'padx': 8, + # 'pady': 8, + } + # self.w1.place(x=0, y=0, width=500, height=450) + self.w1 = self.winfo_toplevel() # a.k.a. root + # self.parent.pack(fill=tk.BOTH) + + self.grid(sticky=tk.NSEW, row=0, column=0) # place *self* + # ^ Only one other widget is in the parent: statusLabel + # self.statusSV = tk.StringVar(master=self.w1) + + self.parent.rowconfigure(0, weight=1) + self.parent.columnconfigure(0, weight=1) + self.row = 0 + self.add_field("service_name", + "TCP Service name (optional, sets host&port)", + gui_class=ttk.Combobox, tooltip="") + self.fields["service_name"].var.trace('w', self.on_service_name_change) + self.add_field("host", "IP address/hostname", + command=self.detect_hosts, + command_text="Detect") + self.add_field( + "port", + "Port", + command=self.default_port, + command_text="Default", + ) + self.add_field( + "localNodeID", + "Local Node ID", + command=self.default_local_node_id, + command_text="Default", + tooltip=('("05.01.01.01.03.01 for Python openlcb examples only:'), + ) + self.unique_ranges_url = "http://registry.openlcb.org/uniqueidranges" + underlined_url = \ + ''.join([letter+'\u0332' for letter in self.unique_ranges_url]) + # ^ '\u0332' is unicode for "underline previous character" + # and is a way of underlining without creating potential + # cross-platform issues when choosing a font name when creating + # a tk.Font instance. + self.local_node_url_label = ttk.Label( + self, + text='See {})'.format(underlined_url), + ) + # A label is not a button, so must bind to mouse button event manually: + self.local_node_url_label.bind( + "", # Mouse button 1 (left click) + lambda e: self.open_url(self.unique_ranges_url) + ) + self.local_node_url_label.grid(row=self.row, + column=self.tooltip_column, + columnspan=self.tooltip_columnspan, + sticky=tk.N) + self.row += 1 + + 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 + ) + + self.add_field( + "device", "Serial Device (or COM port)", + gui_class=ttk.Combobox, + command=lambda: self.load_default("device"), + command_text="Default", + ) + + # 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) + self.statusLabel.grid(sticky=tk.S, row=1, column=0, + columnspan=self.column_count) + # Use the counts determined so far to weight column width equally (use + # same weight): + if self.row > self.row_count: + self.row_count = self.row + if self.column > self.column_count: + self.column_count = self.column + for column in range(self.column_count): + self.columnconfigure(column, weight=1) + # for row in range(self.row_count): + # self.rowconfigure(row, weight=1) + # self.rowconfigure(self.row_count-1, weight=1) # make last row expand + + def on_service_name_change(self, index, value, op): + key = self.fields['service_name'].get() + 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['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): + """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. + 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). + command (function, optional): Command for button. Defaults to None. + command_text (str, optional): Text for button. Defaults to 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). + """ + # self.row should already be set to an empty row. + self.column = 0 # Return to beginning of row + + if command: + if not command_text: + raise ValueError("command_caption is required for command.") + if command_text: + if not command: + raise ValueError("command is required for command_caption.") + + field = DataField() + field.label = ttk.Label(self, text=text) + 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, + ) + field.widget.grid(row=self.row, column=self.column, **self.grid_args) + self.column += 1 + + if command: + field.button = ttk.Button(self, text=command_text, + command=command) + field.button.grid(row=self.row, column=self.column, + **self.grid_args) + self.column += 1 # go to next column even if button wasn't added, + # to keep columns uniform in case another column is added. + + self.row += 1 + + # return field + if tooltip is not None: + # Even if "", still add it. + field.tooltip = ttk.Label(self, text=tooltip) + field.tooltip.grid(row=self.row, column=self.tooltip_column, + columnspan=self.tooltip_columnspan, sticky=tk.N) + # ^ tk.N ("north') is top. Stick to top since the tip describes the + # field.widget above it. + # ^ **self.gridargs is not necessary here (sticky is always tk.N). + self.row += 1 + + if self.column > self.column_count: + self.column_count = self.column + + def open_url(self, url): + import webbrowser + webbrowser.open_new_tab(url) + + def default_local_node_id(self): + self.load_default('localNodeID') + + def default_port(self): + self.load_default('port') + + def load_default(self, key): + self.fields[key].set(self.settings.getDefault(key)) + + def set_status(self, msg): + self.statusLabel.configure(text=msg) + + def set_tooltip(self, key, msg): + self.fields[key].tooltip.configure(text=msg) + + def show_services(self): + self.fields['service_name'].widget['values'] = \ + list(self.detected_services.keys()) + + def update_service(self, zc: Zeroconf, type_: str, name: str) -> None: + if name in self.detected_services: + self.detected_services[name]['type'] = type_ + print(f"Service {name} updated") + else: + self.detected_services[name] = {'type': type_} + print(f"Warning: {name} was not present yet during update.") + self.show_services() + + def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None: + if name in self.detected_services: + del self.detected_services[name] + self.set_status(f"{name} disconnected from the Wi-Fi/LAN") + print(f"Service {name} removed") + else: + print(f"Warning: {name} was already removed.") + self.show_services() + + def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: + """ + This must use name as key, since multiple services can be advertised by + one server! + """ + info = zc.get_service_info(type_, name) + if name not in self.detected_services: + self.detected_services[name] = {} + self.detected_services[name]['type'] = info.type + # By now we only have ones where type==servicetype + # (See detect_hosts) unless servicetype is set to None. + self.detected_services[name]['server'] = info.server # hostname + self.detected_services[name]['port'] = info.port + self.detected_services[name]['properties'] = info.properties + # ^ properties is a dict potentially containing various info + # (no properties are known to be useful in this case) + self.detected_services[name]['addresses'] = info.addresses + # ^ addresses is a list of bytes objects + # other info attributes: priority, weight, added, interface_index + self.set_tooltip( + 'service_name', + f"Found {name} on Wi-Fi/LAN. Select an option above." + ) + print(f"Service {name} added, service info: {info}") + else: + print(f"Warning: {name} was already added.") + self.show_services() + + def detect_hosts(self, servicetype="_openlcb-can._tcp.local."): + if not zeroconf_enabled: + self.set_status("The Python zeroconf package is not installed.") + return + if not self.zeroconf: + self.set_status("Zeroconf was not initialized.") + return + if not self.listener: + self.set_status("Listener was not initialized.") + return + if self.browser: + self.set_status("Already listening for {} devices." + .format(self.servicetype)) + return + self.servicetype = servicetype + self.browser = ServiceBrowser(self.zeroconf, self.servicetype, + self.listener) + self.set_status("Detecting hosts...") + + def detect_nodes(self): + self.set_status("Detecting nodes...") + self.set_status("Detecting nodes...not implemented here." + " See example_node_implementation.") + + def exit_clicked(self): + self.top = self.winfo_toplevel() + self.top.quit() + + +def main(): + root = tk.Tk() + screen_w = root.winfo_screenwidth() + screen_h = root.winfo_screenheight() + window_w = round(screen_w / 2) + window_h = round(screen_h * .75) + root.geometry("{}x{}".format( + window_w, + window_h, + )) # WxH+X+Y format + root.minsize = (window_w, window_h) + mainform = MainForm(root) + mainform.master.title("Python OpenLCB Examples") + try: + mainform.mainloop() + finally: + if mainform.zeroconf: + mainform.zeroconf.close() + mainform.zeroconf = None + return 0 + + +if __name__ == "__main__": + sys.exit(main())