Compare commits

..

19 Commits
v1.1.1 ... main

Author SHA1 Message Date
Nicolas 2a794d5a51 cff-version updated to 1.2.0 2025-08-04 21:31:16 +02:00
Nicolas 0c86ae95e6 add DOI to CITATION.cff 2025-08-04 21:23:23 +02:00
Nicolas 4e1ac7aa5b updated version to v1.1.2 2025-08-04 21:11:43 +02:00
Nicolas c3a29e0e63 Readme: "Adding new terminals" section added 2025-08-04 21:11:43 +02:00
Nicolas 49f654993e Change docs path and update publishing method 2025-08-04 21:11:43 +02:00
Nicolas 0f5eda6cfc py.typed file added indication package uses type annotations 2025-08-04 21:11:43 +02:00
Nicolas 6cb067ce43 CITATION.cff fixed and CITATION.cff validation added to CI 2025-08-04 21:11:43 +02:00
Nicolas ddec81acaa readme updated 2025-08-04 21:11:43 +02:00
Nicolas 6cb3e216a2 version number updated 2025-08-04 21:11:43 +02:00
Nicolas e160fd2162 Missing docstrings added 2025-08-04 21:11:43 +02:00
Nicolas 418bcb59e8 docs: hide '__init__' docstring for bus terminal classes 2025-06-11 12:58:49 +02:00
Nicolas c0c774f8e3 Merge branch 'main' of https://github.com/Nonannet/pyhoff 2025-06-06 16:37:03 +02:00
Nicolas 3de52ae748 Code style fixed 2025-06-06 16:36:55 +02:00
Nicolas 3771cfad05 Docstrings updated 2025-06-06 16:34:51 +02:00
Nicolas 781816334d docs restructured and updated 2025-06-06 16:34:39 +02:00
Nicolas Kruse 0ef833e64b
Docstrings in __init__.py fixed 2025-05-29 01:25:16 +02:00
Nicolas 6febc47de0 code style fixed 2025-05-26 18:02:27 +02:00
Nicolas bd61fa67bd docstrings for terminals updated 2025-05-26 18:00:40 +02:00
Nicolas 15e312d763 docs updated 2025-05-26 18:00:24 +02:00
17 changed files with 195 additions and 97 deletions

View File

@ -12,9 +12,10 @@ exclude =
__pycache__,
build,
dist,
.conda
.venv
venv
.conda,
.venv,
venv,
docs/source/api
# Enable specific plugins or options
# Example: Enabling flake8-docstrings

View File

@ -27,6 +27,13 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install -e .[dev]
if [ "${{ matrix.python-version }}" = "3.13" ]; then
python -m pip install cffconvert
fi
- name: Validate CITATION.cff
if: ${{ matrix.python-version == '3.13' }}
run: cffconvert --validate
- name: Lint code with flake8
run: flake8

View File

@ -9,7 +9,7 @@ permissions:
contents: write
jobs:
build-and-deploy:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -17,21 +17,36 @@ jobs:
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: Install package and dependencies
run: pip install .[doc_build]
- name: Generate Class List
run: |
pip install .
python ./docs/source/generate_class_list.py
run: python ./docs/source/generate_class_list.py
- name: Build Docs
run: |
cp LICENSE docs/source/LICENSE.md
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
mkdir -p ./build/html/_autogenerated
cp ./build/html/api/* ./build/html/_autogenerated/
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
branch: gh-pages
folder: docs/build/html
path: docs/build/html
deploy:
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@ -10,6 +10,10 @@ jobs:
name: Build and publish
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/${{ github.event.repository.name }}/
steps:
- uses: actions/checkout@v3

1
.gitignore vendored
View File

@ -70,6 +70,7 @@ instance/
# Sphinx documentation
docs/_build/
docs/source/api/
# Autogenerated documentation
docs/source/modules.md

View File

@ -1,15 +1,16 @@
cff-version: 1.2.0
message: "If you use this software, please cite it as below."
title: pyhoff
abstract: The pyhoff package allows easy accessing of Beckhoff and Wago terminals with python over ModBus TCP
authors:
- family-names: Kruse
given-names: Nicolas
orcid: "https://orcid.org/0000-0001-6758-2269"
version: 1.1.0
#date-released: "2025-04-01"
#identifiers:
# - description: This is the collection of archived snapshots of all versions of My Research Software
# type: doi
# value: "10.5281/"
license: MIT License
version: v1.1.2
date-released: "2025-08-04"
identifiers:
- description: This is the collection of archived snapshots of all versions of pyhoff
type: doi
value: "10.5281/zenodo.16740202"
license: MIT
repository-code: "https://github.com/Nonannet/pyhoff"

View File

@ -1,6 +1,5 @@
# Pyhoff
## Description
The pyhoff package allows you to read and write the most common
Beckhoff and WAGO bus terminals ("Busklemmen") using the Ethernet bus
coupler ("Busskoppler") BK9000, BK9050, BK9100, or WAGO 750_352
@ -55,6 +54,62 @@ bk.select(KL4002, 0).set_voltage(1, 4.2)
```
## Adding new terminals
The package comes with automatic generated code stubs for nearly all
terminals. These stubs are not tested with hardware but for most
digital IO terminals the code should be fully functional.
Such a stub looks like this:
```python
# From ./src/pyhoff/devices.py:
class KL2442(DigitalOutputTerminal):
"""
KL2442: 2-channel digital output, 24 V DC, 2 x 4 A/1 x 8 A
(Automatic generated stub)
"""
parameters = {'output_bit_width': 2, 'input_bit_width': 0}
```
For analog IO terminals the stubs are functional as well,
but they provide only a generic `read_channel_word` and
`read_normalized` function (for inputs) without scaling the
values to voltages, currents or temperatures. For better usability
they might be extended with functions. Based on the stub the
extension could look like this:
```python
from pyhoff.devices import KL3054 as KL3054_stub
class KL3054(KL3054_stub):
def read_current(self, channel: int) -> float:
return self.read_normalized(channel) * 16.0 + 4.0
```
Or for contributing to the pyhoff package, the existing stub
code can be updated like this:
```python
# From ./src/pyhoff/devices.py:
class KL3054(AnalogInputTerminal):
"""
KL3054: 4x analog input 4...20 mA 12 Bit single-ended
"""
# Input: 4 x 16 Bit Daten (optional 4x 8 Bit Control/Status)
parameters = {'input_word_width': 4}
def read_current(self, channel: int) -> float:
"""
Read the current value from a specific channel.
Args:
channel: The channel number to read from.
Returns:
The current value in mA.
"""
return self.read_normalized(channel) * 16.0 + 4.0
```
## Contributing
Other analog and digital IO terminals are easy to complement. Contributions are welcome!
Please open an issue or submit a pull request on GitHub.
@ -62,32 +117,24 @@ Please open an issue or submit a pull request on GitHub.
## Developer Guide
To get started with developing the `pyhoff` package, follow these steps:
1. **Clone the Repository**
First, clone the repository to your local machine using Git:
1. First, clone the repository to your local machine using Git:
```bash
git clone https://github.com/Nonannet/pyhoff.git
cd pyhoff
```
2. **Set Up a Virtual Environment**
It is recommended to use a virtual environment to manage dependencies. You can create one using `venv`:
2. It is recommended to use a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # On Windows use `venv\Scripts\activate`
python -m venv .venv
source .venv/bin/activate # On Windows/Powershell use `.\venv\Scripts\Activate.ps1`
```
3. **Install Dev Dependencies**
Install pyhoff from source plus the dependencies required for development using `pip`:
3. Install pyhoff from source plus the development dependencies:
```bash
pip install -e .[dev]
```
4. **Run Tests**
Ensure that everything is set up correctly by running the tests:
4. Ensure that everything is set up correctly by running the tests:
```bash
pytest
```

View File

@ -26,7 +26,8 @@ exclude_patterns = []
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
# html_theme = 'alabaster'
html_theme = 'sphinx_rtd_theme'
html_theme = 'pydata_sphinx_theme'
html_static_path = ['_static']
autodoc_inherit_docstrings = True
autoclass_content = 'both'

View File

@ -2,43 +2,67 @@ import importlib
import inspect
import fnmatch
from io import TextIOWrapper
import os
def write_classes(f: TextIOWrapper, patterns: list[str], module_name: str, title: str, description: str = '', exclude: list[str] = []) -> None:
"""Write the classes to the file."""
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__)
(obj.__doc__ and ('(Automatic generated stub)' not in obj.__doc__ or ' digital ' in obj.__doc__)) and
obj.__doc__ and '(no I/O function)' not in obj.__doc__
)
]
"""Write the classes to the file."""
f.write(f'## {title}\n\n')
if description:
f.write(f'{description}\n\n')
write_dochtree(f, title, classes)
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(f'docs/source/api/{cls}.md', 'w') as f2:
f2.write(f'# {module_name}.{cls}\n')
f2.write('```{eval-rst}\n')
f2.write(f'.. autoclass:: {module_name}.{cls}\n')
f2.write(' :members:\n')
f2.write(' :show-inheritance:\n')
f2.write(' :inherited-members:\n')
if title not in ['Base classes', 'Bus coupler']:
f2.write(' :exclude-members: select, parameters\n')
if 'bus terminals' in title:
f2.write(' :class-doc-from: class\n')
f2.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.')
def write_dochtree(f: TextIOWrapper, title: str, items: list[str]):
f.write('```{toctree}\n')
f.write(':maxdepth: 1\n')
f.write(f':caption: {title}:\n')
# f.write(':hidden:\n')
for text in items:
if not text.startswith('_'):
f.write(f"{text}\n")
f.write('```\n\n')
if __name__ == "__main__":
# Ensure the output directory exists
os.makedirs('docs/source/api', exist_ok=True)
with open('docs/source/api/index.md', 'w') as f:
f.write('# Classes and Modules\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, ['*Terminal*'], 'pyhoff.devices', title='Generic bus terminals')
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,9 +1,8 @@
```{toctree}
:maxdepth: 2
:caption: Contents:
readme
modules
:maxdepth: 1
:hidden:
api/index
repo
```
```{include} ../../README.md

View File

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

3
docs/source/repo.md Normal file
View File

@ -0,0 +1,3 @@
# Code repository
Code repository is on GitHub: [github.com/Nonannet/pyhoff](https://github.com/Nonannet/pyhoff).

View File

@ -1,6 +1,6 @@
[project]
name = "pyhoff"
version = "1.1.1"
version = "1.1.2"
authors = [
{ name="Nicolas Kruse", email="nicolas.kruse@nonan.net" },
]
@ -25,10 +25,19 @@ build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
pyhoff = ["py.typed"]
[project.optional-dependencies]
dev = [
"pytest", "flake8", "mypy"
]
doc_build = [
"sphinx",
"pydata_sphinx_theme",
"sphinx-autodoc-typehints",
"myst-parser"
]
[tool.mypy]
files = ["src"]

View File

@ -137,9 +137,10 @@ class AnalogInputTerminal(BusTerminal):
Args:
channel: The channel number (1 based index) to read from.
error_value: Value that is returned in case the modbus read command fails.
Returns:
The read word value.
The read word value or provided error_value if read failed.
Raises:
Exception: If the word offset or count is out of range.
@ -174,6 +175,7 @@ class AnalogOutputTerminal(BusTerminal):
Args:
channel: The channel number (1 based index) to read from.
error_value: Value that is returned in case the modbus read command fails.
Returns:
The read word value or provided error_value if read failed.
@ -225,19 +227,10 @@ class BusCoupler():
"""
Base class for ModBus TCP bus coupler
Args:
host: ip or hostname of the bus coupler
port: port of the modbus host
debug: outputs modbus debug information
timeout: timeout for waiting for the device response
watchdog: time in seconds after the device sets all outputs to
default state. A value of 0 deactivates the watchdog.
debug: If True, debug information is printed.
Attributes:
bus_terminals: A list of bus terminal classes according to the
bus_terminals (list[BusTerminal]): A list of bus terminal classes according to the
connected terminals.
modbus: The underlying modbus client used for the connection.
modbus (SimpleModbusClient): The underlying modbus client used for the connection.
"""
def __init__(self, host: str, port: int = 502, bus_terminals: Iterable[type[BusTerminal]] = [],
@ -248,6 +241,8 @@ class BusCoupler():
Args:
host: ip or hostname of the bus coupler
port: port of the modbus host
bus_terminals: list of bus terminal classes for the
connected terminals
debug: outputs modbus debug information
timeout: timeout for waiting for the device response
watchdog: time in seconds after the device sets all outputs to

View File

@ -224,7 +224,7 @@ class KL3054(AnalogInputTerminal):
channel: The channel number to read from.
Returns:
The current value.
The current value in mA.
"""
return self.read_normalized(channel) * 16.0 + 4.0
@ -244,7 +244,7 @@ class KL3042(AnalogInputTerminal):
channel: The channel number to read from.
Returns:
The current value.
The current value in mA.
"""
return self.read_normalized(channel) * 20.0
@ -311,7 +311,7 @@ class KL4002(AnalogOutputTerminal):
Args:
channel: The channel number to set.
value: The voltage value to set.
value: The voltage value to set in V.
Returns:
True if the write operation succeeded.
@ -348,7 +348,7 @@ class KL4132(AnalogOutputTerminal):
Args:
channel: The channel number to set.
value: The voltage value to set.
value: The voltage value to set in V.
Returns:
True if the write operation succeeded.
@ -369,7 +369,7 @@ class KL4004(AnalogOutputTerminal):
Args:
channel: The channel number to set.
value: The voltage value to set.
value: The voltage value to set in V.
Returns:
True if the write operation succeeded.

View File

@ -48,20 +48,13 @@ class SimpleModbusClient:
"""
A simple Modbus TCP client
Args:
host: hostname or IP address
port: server port
unit_id: ModBus id
timeout: socket timeout in seconds
debug: if True prints out transmitted and received bytes in hex
Attributes:
host: hostname or IP address
port: server port
unit_id: ModBus id
timeout: socket timeout in seconds
last_error: contains last error message or empty string if no error occurred
debug: if True prints out transmitted and received bytes in hex
host (str): hostname or IP address
port (int): server port
unit_id (int): ModBus id
timeout (float): socket timeout in seconds
last_error (str): contains last error message or empty string if no error occurred
debug (bool): if True prints out transmitted and received bytes in hex
"""

0
src/pyhoff/py.typed Normal file
View File