diff --git a/.gitignore b/.gitignore index 78b22e2..2b6b14e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,6 @@ /*.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 +# ^ /.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) +/build +/doc/_autosummary diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..26217e7 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = doc +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/README.md b/README.md index 90884c3..c51335a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Linux: mkdir -p ~/.virtualenvs python3 -m venv ~/.virtualenvs/pytest-env source ~/.virtualenvs/pytest-env/bin/activate -``` +``` Windows: ``` @@ -51,11 +51,21 @@ If using VSCode (or fully open-source VSCodium): - (recommended) **autoDocstring**: Type `"""` below a method or class and it will create a Sphinx-style template for you. - The workspace file has `"autoDocstring.docstringFormat": "google"` set since Google style is widely used and comprehensive (documents types etc). +#### Documentation +The sources for building documentation are: +- rst (reStructuredText) file(s) in the doc directory +- Google-style docstrings which is one of the formats recognized by Sphinx (and one with a thorough explanation of input and output types). + +To generate documentation that can be placed on a website (such as could be published to readthedocs.io automatically) or provided to end users, run: +``` +make html +``` + ### Testing To run the unit test suite: ``` python3 -m pip install --user pytest -# ^ or use a +# ^ or use a python3 -m pytest # or to auto-detect test and run with standard log level: # python3 -m pytest tests @@ -84,7 +94,7 @@ python3 example_node_implementation.py 192.168.1.40 python3 example_node_implementation.py 192.168.1.40:12021 ``` -There's also a serial-port based example. +There's also a serial-port based example. ``` python3 example_string_serial_interface.py /dev/cu.ProperSerialPort ``` diff --git a/doc/_templates/custom-class-template.rst b/doc/_templates/custom-class-template.rst new file mode 100644 index 0000000..b29757c --- /dev/null +++ b/doc/_templates/custom-class-template.rst @@ -0,0 +1,32 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: + :show-inheritance: + :inherited-members: + + {% block methods %} + .. automethod:: __init__ + + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/doc/_templates/custom-module-template.rst b/doc/_templates/custom-module-template.rst new file mode 100644 index 0000000..f46f9f6 --- /dev/null +++ b/doc/_templates/custom-module-template.rst @@ -0,0 +1,66 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Module Attributes') }} + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + :toctree: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + :toctree: + :template: custom-class-template.rst + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + :toctree: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. rubric:: Modules + +.. autosummary:: + :toctree: + :template: custom-module-template.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..4494ec5 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,38 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + +project = 'python-openlcb' +copyright = '2024, Bob Jacobsen' +author = 'Bob Jacobsen' +release = '0.1.0' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.coverage', + 'sphinx.ext.napoleon', # understands NumPy and Google docstrings. +] +autosummary_generate = True # Turn on sphinx.ext.autosummary + +templates_path = ['_templates'] +exclude_patterns = [] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'alabaster' +html_static_path = ['_static'] diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..cc60ca8 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,49 @@ +.. python-openlcb documentation master file, created by + sphinx-quickstart on Thu May 16 16:30:21 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to python-openlcb's documentation! +========================================== +.. autosummary:: + :toctree: _autosummary + :template: custom-module-template.rst + :recursive: + + openlcb + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. image:: Overview.png + :width: 400 + :alt: Flowchart of the software-defined node's network connection + +For how this documentation is built into html, see README.md or create a github workflow to publish it to readthedocs.io. + +For general info on how the "docbuild" was configured, see the following sources which where used: + +- Install Sphinx: https://www.sphinx-doc.org/en/master/usage/installation.html + +- Setup autodoc: https://eikonomega.medium.com/getting-started-with-sphinx-autodoc-part-1-2cebbbca5365 + +- Setup recursive gathering of docstrings using autosummary: https://stackoverflow.com/a/62613202/4541104 + +Instructions for adding new documentation: + +- Use Google-style docstrings (Sphinx will automatically generate documentation sections for such docstrings when `make html` runs). + +- For bullet lists within docstrings, reStructuredText must be used (blank line before each bullet). + +- For additional text that is manually entered (not generated from docstrings), + use the reStructuredText format and add data to this file or other rst files + (in the "doc" folder in the case of this project, as configured in make.bat + and Makefile). + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/example_memory_length_query.py b/example_memory_length_query.py new file mode 100644 index 0000000..c7b1aaa --- /dev/null +++ b/example_memory_length_query.py @@ -0,0 +1,147 @@ +''' +Demo of using the memory service to get the length of a node memory + +Usage: +python3 example_memory_transfer.py [host|host:port] + +Options: +host|host:port (optional) Set the address (or using a colon, + the address and port). Defaults to a hard-coded test + address and port. +''' + + +from openlcb.canbus.tcpsocket import TcpSocket + +from openlcb.canbus.canphysicallayergridconnect import ( + CanPhysicalLayerGridConnect, +) +from openlcb.canbus.canlink import CanLink +from openlcb.nodeid import NodeID +from openlcb.datagramservice import ( + # DatagramWriteMemo, + # DatagramReadMemo, + DatagramService, +) +from openlcb.memoryservice import ( + MemoryReadMemo, + # MemoryWriteMemo, + MemoryService, +) + +# specify connection information +# 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 +from examples_settings import Settings +settings = Settings() + +if __name__ == "__main__": + settings.load_cli_args(docstring=__doc__) +# endregion same code as other examples + +s = TcpSocket() +# s.settimeout(30) +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") + + +def sendToSocket(string): + print(" SR: {}".format(string.strip())) + s.send(string) + + +def printFrame(frame): + print(" RL: {}".format(frame)) + + +canPhysicalLayerGridConnect = CanPhysicalLayerGridConnect(sendToSocket) +canPhysicalLayerGridConnect.registerFrameReceivedListener(printFrame) + + +def printMessage(message): + print("RM: {} from {}".format(message, message.source)) + + +canLink = CanLink(NodeID(settings['localNodeID'])) +canLink.linkPhysicalLayer(canPhysicalLayerGridConnect) +canLink.registerMessageReceivedListener(printMessage) + +datagramService = DatagramService(canLink) +canLink.registerMessageReceivedListener(datagramService.process) + + +def printDatagram(memo): + """create a call-back to print datagram contents when received + + Args: + memo (DatagramReadMemo): The datagram received + + Returns: + bool: Always False (True would mean we sent a reply to this datagram, + but let MemoryService do that). + """ + print("Datagram receive call back: {}".format(memo.data)) + return False + + +datagramService.registerDatagramReceivedListener(printDatagram) + +memoryService = MemoryService(datagramService) + + +# def memoryReadSuccess(memo): +# """createcallbacks to get results of memory read +# +# Args: +# memo (MemoryReadMemo): Event that was generated. +# """ +# print("successful memory read: {}".format(memo.data)) +# +# +# def memoryReadFail(memo): +# print("memory read failed: {}".format(memo.data)) + +def memoryLengthReply(address) : + print ("memory length reply: "+str(address)) + +####################### + +# have the socket layer report up to bring the link layer up and get an alias +print(" SL : link up") +canPhysicalLayerGridConnect.physicalLayerUp() + + +def memoryRequest(): + """Create and send a read datagram. + This is a read of 20 bytes from the start of CDI space. + We will fire it on a separate thread to give time for other nodes to reply + to AME + """ + import time + time.sleep(1) + + # request the length of the CDI space +# memMemo = MemoryReadMemo(NodeID(settings['farNodeID']), +# 64, 0xFF, 0, memoryReadFail, +# memoryReadSuccess) + memoryService.requestSpaceLength(0xFF, NodeID(settings['farNodeID']), memoryLengthReply) + + +import threading # noqa E402 +thread = threading.Thread(target=memoryRequest) +thread.start() + +# process resulting activity +while True: + received = s.receive() + print(" RR: {}".format(received.strip())) + # pass to link processor + canPhysicalLayerGridConnect.receiveString(received) diff --git a/examples_gui.py b/examples_gui.py index 3ace9b5..bc005df 100644 --- a/examples_gui.py +++ b/examples_gui.py @@ -73,8 +73,10 @@ class MainForm(ttk.Frame): 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. diff --git a/make.bat b/make.bat new file mode 100644 index 0000000..976e41b --- /dev/null +++ b/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=doc +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/openlcb/canbus/canlink.py b/openlcb/canbus/canlink.py index 3871299..58ac3b5 100644 --- a/openlcb/canbus/canlink.py +++ b/openlcb/canbus/canlink.py @@ -10,10 +10,12 @@ This implementation handles one static Local Node and a variable number of Remote Nodes. - - An alias is allocated for the Local Node when the link comes up. - - Aliases are tracked for the Remote Nodes, but not allocated here - Multi-frame addressed messages are accumulated in parallel +- An alias is allocated for the Local Node when the link comes up. + +- Aliases are tracked for the Remote Nodes, but not allocated here + +Multi-frame addressed messages are accumulated in parallel ''' from enum import Enum @@ -623,8 +625,11 @@ def canHeaderToFullFormat(self, frame): class AccumKey: '''Class that holds the ID for accumulating a multi-part message: + - MTI + - Source + - Destination Together these uniquely identify a stream of frames that need to diff --git a/openlcb/canbus/seriallink.py b/openlcb/canbus/seriallink.py index 5d32c68..3901ea7 100644 --- a/openlcb/canbus/seriallink.py +++ b/openlcb/canbus/seriallink.py @@ -1,5 +1,5 @@ ''' -simple serial nput for string send and receive +simple serial input for string send and receive expects prior setting of device name ''' import serial @@ -8,6 +8,7 @@ class SerialLink: + """simple serial input for string send and receive""" def __init__(self): pass @@ -42,9 +43,12 @@ def send(self, string): def receive(self): '''Receive at least one GridConnect frame and return as string. - - Guarantee: If input is valid, there will be at least one ";" in the - response. + + - Guarantee: If input is valid, there will be at least one ";" + in the response. + - This makes it nicer to display the raw data. + - Note that the response may end with a partial frame. Returns: diff --git a/openlcb/canbus/tcpsocket.py b/openlcb/canbus/tcpsocket.py index 094bdbf..9f528a3 100644 --- a/openlcb/canbus/tcpsocket.py +++ b/openlcb/canbus/tcpsocket.py @@ -39,9 +39,12 @@ def send(self, string): def receive(self): '''Receive at least one GridConnect frame and return as string. + - Guarantee: If input is valid, there will be at least one ";" in the response. + - This makes it nicer to display the raw data. + - Note that the response may end with a partial frame. Returns: diff --git a/openlcb/memoryservice.py b/openlcb/memoryservice.py index 6dab088..c14f85f 100644 --- a/openlcb/memoryservice.py +++ b/openlcb/memoryservice.py @@ -109,8 +109,10 @@ def spaceDecode(self, space): def requestMemoryRead(self, memo): '''Request a read operation start. + - If okReply in the memo is triggered, it will be followed by a dataReply. + - A rejectedReply will not be followed by a dataReply. Args: @@ -227,10 +229,10 @@ def datagramReceivedListener(self, dmemo): self.spaceLengthCallback = None return True # normal reply - address = (int(dmemo.data[3]) << 24 - + int(dmemo.data[4]) << 16 - + int(dmemo.data[5]) << 8 - + int(dmemo.data[6])) + address = (int(dmemo.data[3]) << 24) \ + + (int(dmemo.data[4]) << 16) \ + + (int(dmemo.data[5]) << 8) \ + + int(dmemo.data[6]) self.spaceLengthCallback(address) self.spaceLengthCallback = None else: @@ -268,7 +270,7 @@ def requestSpaceLength(self, space, nodeID, callback): # send request dgReqMemo = DatagramWriteMemo( nodeID, - [DatagramService.ProtocolID.MemoryOperation.rawValue, 0x84, space] + [DatagramService.ProtocolID.MemoryOperation.value, 0x84, space] ) self.service.sendDatagram(dgReqMemo) diff --git a/openlcb/remotenodestore.py b/openlcb/remotenodestore.py index 512550b..df48642 100644 --- a/openlcb/remotenodestore.py +++ b/openlcb/remotenodestore.py @@ -48,6 +48,7 @@ def createNewRemoteNode(self, message) : A new node was found by checkForNewNode, so this mutates the store to add this. This should only be called if checkForNewNode is true to avoid excess publishing! + - Parameter message: Incoming message to process ''' # need to create the node and process it's New_Node_Seen