Compare commits

..

No commits in common. "2acffb90b5a71611d2792427b0b1643d87c98954" and "5aab2d6d74a9a49118584ceb481d6f3e71c2c613" have entirely different histories.

15 changed files with 15 additions and 295 deletions

View File

@ -1,37 +0,0 @@
name: Build and Deploy Docs
on:
push:
branches:
- main
permissions:
contents: write
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.x"
- name: Install dependencies
run: pip install sphinx sphinx_rtd_theme sphinx-autodoc-typehints myst-parser
- name: Generate Class List
run: |
pip install .
python ./docs/source/generate_class_list.py
- name: Build Docs
run: |
cd docs
sphinx-apidoc -o ./source/ ../src/ -M --no-toc
rm ./source/*.rst
make html
touch ./build/html/.nojekyll
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: gh-pages
folder: docs/build/html

View File

@ -1,35 +0,0 @@
name: Publish to PyPI
on:
push:
tags:
- "v*"
jobs:
publish:
name: Build and publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Ensure this is main branch
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
run: echo "Proceeding with publish"
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install build tools
run: python -m pip install --upgrade build twine
- name: Build package
run: python -m build
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: python -m twine upload dist/*

3
.gitignore vendored
View File

@ -71,9 +71,6 @@ instance/
# Sphinx documentation
docs/_build/
# Autogenerated documentation
docs/source/modules.md
# PyBuilder
.pybuilder/
target/

View File

@ -5,7 +5,7 @@ authors:
- family-names: Kruse
given-names: Nicolas
orcid: "https://orcid.org/0000-0001-6758-2269"
version: 1.1.0
version: 1.0.2
#date-released: "2025-04-01"
#identifiers:
# - description: This is the collection of archived snapshots of all versions of My Research Software

View File

@ -1,4 +1,4 @@
# Pyhoff
# pyhoff
## Description
The pyhoff package allows you to read and write the most common
@ -43,15 +43,15 @@ bk.add_bus_terminals(KL2404, KL2424, KL9100, KL1104, KL3202,
KL4004, KL9010)
# Set 1. output of the first KL2404-type bus terminal to hi
bk.select(KL2404, 0).write_coil(1, True)
KL2404.select(bk, 0).write_coil(1, True)
# read temperature from the 2. channel of the 2. KL3202-type
# bus terminal
t = bk.select(KL3202, 1).read_temperature(2)
t = KL3202.select(bk, 1).read_temperature(2)
print(f"t = {t:.1f} °C")
# Set 1. output of the 1. KL4002-type bus terminal to 4.2 V
bk.select(KL4002, 0).set_voltage(1, 4.2)
KL4002.select(bk, 0).set_voltage(1, 4.2)
```

View File

@ -1,20 +0,0 @@
# 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 = source
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)

View File

@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
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

View File

@ -1,32 +0,0 @@
# 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("../src/"))
project = 'pyhoff'
copyright = '2025, Nicolas Kruse'
author = 'Nicolas Kruse'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", "myst_parser"]
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_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
autodoc_inherit_docstrings = True

View File

@ -1,44 +0,0 @@
import importlib
import inspect
import fnmatch
from io import TextIOWrapper
def write_classes(f: TextIOWrapper, patterns: list[str], module_name: str, title: str, description: str = '', exclude: list[str] = []) -> None:
module = importlib.import_module(module_name)
classes = [
name for name, obj in inspect.getmembers(module, inspect.isclass)
if (obj.__module__ == module_name and
any(fnmatch.fnmatch(name, pat) for pat in patterns if pat not in exclude) and
obj.__doc__ and '(Automatic generated stub)' not in obj.__doc__)
]
"""Write the classes to the file."""
f.write(f'## {title}\n\n')
if description:
f.write(f'{description}\n\n')
for cls in classes:
f.write('```{eval-rst}\n')
f.write(f'.. autoclass:: {module_name}.{cls}\n')
f.write(' :members:\n')
f.write(' :undoc-members:\n')
f.write(' :show-inheritance:\n')
f.write(' :inherited-members:\n')
if title != 'Base classes':
f.write(' :exclude-members: select\n')
f.write('```\n\n')
with open('docs/source/modules.md', 'w') as f:
f.write('# Classes\n\n')
write_classes(f, ['BK*', 'WAGO_750_352'], 'pyhoff.devices', title='Bus coupler',
description='These classes are bus couplers and are used to connect the IO bus terminals to a Ethernet interface.')
write_classes(f, ['KL*'], 'pyhoff.devices', title='Beckhoff bus terminals')
write_classes(f, ['WAGO*'], 'pyhoff.devices', title='WAGO bus terminals', exclude=['WAGO_750_352'])
write_classes(f, ['*'], 'pyhoff', title='Base classes',
description='These classes are base classes for devices and are typically not used directly.')
write_classes(f, ['*'], 'pyhoff.modbus', title='Modbus',
description='This modbus implementation is used internally.')

View File

@ -1,10 +0,0 @@
```{toctree}
:maxdepth: 2
:caption: Contents:
readme
modules
```
```{include} ../../README.md
```

View File

@ -1,2 +0,0 @@
```{include} ../../README.md
```

View File

@ -1,22 +1,21 @@
[project]
name = "pyhoff"
version = "1.1.1"
version = "1.0.2"
authors = [
{ name="Nicolas Kruse", email="nicolas.kruse@nonan.net" },
]
description = "The pyhoff package allows easy accessing of Beckhoff and Wago terminals with python over ModBus TCP"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
requires-python = ">=3.9"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
[project.urls]
Homepage = "https://github.com/Nonannet/pyhoff"
Issues = "https://github.com/Nonannet/pyhoff/issues"
documentation = "https://nonannet.github.io/pyhoff/"
[build-system]
requires = ["setuptools>=61.0", "wheel"]

View File

@ -1,10 +1,8 @@
from .modbus import SimpleModbusClient
from typing import Iterable, TypeVar
_BT = TypeVar('_BT', bound='BusTerminal')
from typing import Type, Iterable
def _is_bus_terminal(bt_type: type['BusTerminal']) -> bool:
def _is_bus_terminal(bt_type: Type['BusTerminal']) -> bool:
if BusTerminal.__name__ == bt_type.__name__:
return True
@ -45,20 +43,7 @@ class BusTerminal():
self._mixed_mapping = mixed_mapping
@classmethod
def select(cls: type[_BT], bus_coupler: 'BusCoupler', terminal_number: int = 0) -> _BT:
"""
Returns the n-th bus terminal instance of the parent class
specified by terminal_number.
Args:
bus_coupler: The bus coupler to which the terminal is connected.
terminal_number: The index of the bus terminal to return. Counted for
all bus terminals of the same type, not all bus terminals. Started for the
first terminal with 0
Returns:
The selected bus terminal instance.
"""
def select(cls, bus_coupler: 'BusCoupler', terminal_number: int = 0) -> 'BusTerminal':
terminal_list = [bt for bt in bus_coupler.bus_terminals if isinstance(bt, cls)]
assert terminal_list, f"No instance of {cls.__name__} configured at this BusCoupler"
assert 0 <= terminal_number < len(terminal_list), f"Out of range, select in range: 0..{len(terminal_list) - 1}"
@ -240,7 +225,7 @@ class BusCoupler():
modbus: The underlying modbus client used for the connection.
"""
def __init__(self, host: str, port: int = 502, bus_terminals: Iterable[type[BusTerminal]] = [],
def __init__(self, host: str, port: int = 502, bus_terminals: Iterable[Type[BusTerminal]] = [],
timeout: float = 5, watchdog: float = 0, debug: bool = False):
"""
Instantiate a new bus coupler base class.
@ -278,7 +263,7 @@ class BusCoupler():
def _init_hardware(self, watchdog: float) -> None:
pass
def add_bus_terminals(self, *new_bus_terminals: type[BusTerminal] | Iterable[type[BusTerminal]]) -> list[BusTerminal]:
def add_bus_terminals(self, *new_bus_terminals: Type[BusTerminal] | Iterable[Type[BusTerminal]]) -> list[BusTerminal]:
"""
Add bus terminals to the bus coupler.
@ -289,7 +274,7 @@ class BusCoupler():
The corresponding list of bus terminal objects.
"""
terminal_classes: list[type[BusTerminal]] = []
terminal_classes: list[Type[BusTerminal]] = []
for element in new_bus_terminals:
if isinstance(element, Iterable):
for bt in element:
@ -331,28 +316,6 @@ class BusCoupler():
return self.bus_terminals
def select(self, bus_terminal_type: type[_BT], terminal_number: int = 0) -> _BT:
"""
Returns the n-th bus terminal instance of the given bus terminal type and
terminal index.
Args:
bus_terminals_type: The bus terminal class to select from.
terminal_number: The index of the bus terminal to return. Counted for
all bus terminals of the same type, not all bus terminals. Started for the
first terminal with 0
Returns:
The selected bus terminal instance.
Example:
>>> from pyhoff.devices import *
>>> bk = BK9050("172.16.17.1", bus_terminals=[KL2404, KL2424])
>>> # Select the first KL2425 terminal:
>>> kl2404 = bk.select(KL2424, 0)
"""
return bus_terminal_type.select(self, terminal_number)
def get_error(self) -> str:
"""
Get the last error message.

View File

@ -330,7 +330,7 @@ class SimpleModbusClient:
address: The register address to read from.
Returns:
The value of the coil or None if error
The value of the coil.
"""
value = self.read_coils(address)
if value:

View File

@ -1,24 +0,0 @@
from pyhoff.devices import KL2404, KL2424, KL9100, KL1104, \
KL3202, KL4002, KL9188, KL3054, KL3214, KL4004, KL9010, BK9050
def test_readme_example():
# connect to the BK9050 by tcp/ip on default port 502
bk = BK9050("172.16.17.1")
# add all bus terminals connected to the bus coupler
# in the order of the physical arrangement
bk.add_bus_terminals(KL2404, KL2424, KL9100, KL1104, KL3202,
KL3202, KL4002, KL9188, KL3054, KL3214,
KL4004, KL9010)
# Set 1. output of the first KL2404-type bus terminal to hi
bk.select(KL2404, 0).write_coil(1, True)
# read temperature from the 2. channel of the 2. KL3202-type
# bus terminal
t = bk.select(KL3202, 1).read_temperature(2)
print(f"t = {t:.1f} °C")
# Set 1. output of the 1. KL4002-type bus terminal to 4.2 V
bk.select(KL4002, 0).set_voltage(1, 4.2)