mirror of https://github.com/Nonannet/pyhoff.git
Compare commits
No commits in common. "main" and "v1.1" have entirely different histories.
7
.flake8
7
.flake8
|
@ -12,10 +12,9 @@ exclude =
|
|||
__pycache__,
|
||||
build,
|
||||
dist,
|
||||
.conda,
|
||||
.venv,
|
||||
venv,
|
||||
docs/source/api
|
||||
.conda
|
||||
.venv
|
||||
venv
|
||||
|
||||
# Enable specific plugins or options
|
||||
# Example: Enabling flake8-docstrings
|
||||
|
|
|
@ -27,13 +27,6 @@ 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
|
||||
|
|
|
@ -9,7 +9,7 @@ permissions:
|
|||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -17,36 +17,21 @@ jobs:
|
|||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install package and dependencies
|
||||
run: pip install .[doc_build]
|
||||
- name: Install dependencies
|
||||
run: pip install sphinx sphinx_rtd_theme sphinx-autodoc-typehints myst-parser
|
||||
- name: Generate Class List
|
||||
run: python ./docs/source/generate_class_list.py
|
||||
run: |
|
||||
pip install .
|
||||
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
|
||||
mkdir -p ./build/html/_autogenerated
|
||||
cp ./build/html/api/* ./build/html/_autogenerated/
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
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
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
branch: gh-pages
|
||||
folder: docs/build/html
|
||||
|
|
|
@ -10,10 +10,6 @@ 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
|
||||
|
||||
|
|
|
@ -70,7 +70,6 @@ instance/
|
|||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
docs/source/api/
|
||||
|
||||
# Autogenerated documentation
|
||||
docs/source/modules.md
|
||||
|
|
15
CITATION.cff
15
CITATION.cff
|
@ -1,16 +1,15 @@
|
|||
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: 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
|
||||
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
|
||||
repository-code: "https://github.com/Nonannet/pyhoff"
|
77
README.md
77
README.md
|
@ -1,5 +1,6 @@
|
|||
# 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
|
||||
|
@ -54,62 +55,6 @@ 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.
|
||||
|
@ -117,24 +62,32 @@ 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. First, clone the repository to your local machine using Git:
|
||||
1. **Clone the Repository**
|
||||
First, clone the repository to your local machine using Git:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Nonannet/pyhoff.git
|
||||
cd pyhoff
|
||||
```
|
||||
|
||||
2. It is recommended to use a virtual environment:
|
||||
2. **Set Up a Virtual Environment**
|
||||
It is recommended to use a virtual environment to manage dependencies. You can create one using `venv`:
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # On Windows/Powershell use `.\venv\Scripts\Activate.ps1`
|
||||
python -m venv venv
|
||||
source venv/bin/activate # On Windows use `venv\Scripts\activate`
|
||||
```
|
||||
|
||||
3. Install pyhoff from source plus the development dependencies:
|
||||
3. **Install Dev Dependencies**
|
||||
Install pyhoff from source plus the dependencies required for development using `pip`:
|
||||
|
||||
```bash
|
||||
pip install -e .[dev]
|
||||
```
|
||||
|
||||
4. Ensure that everything is set up correctly by running the tests:
|
||||
4. **Run Tests**
|
||||
Ensure that everything is set up correctly by running the tests:
|
||||
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
|
|
|
@ -26,8 +26,7 @@ exclude_patterns = []
|
|||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
# html_theme = 'alabaster'
|
||||
html_theme = 'pydata_sphinx_theme'
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_static_path = ['_static']
|
||||
|
||||
autodoc_inherit_docstrings = True
|
||||
autoclass_content = 'both'
|
||||
|
|
|
@ -2,66 +2,42 @@ 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__ or ' digital ' in obj.__doc__)) and
|
||||
obj.__doc__ and '(no I/O function)' not in obj.__doc__
|
||||
)
|
||||
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')
|
||||
|
||||
write_dochtree(f, title, classes)
|
||||
|
||||
for cls in classes:
|
||||
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')
|
||||
|
||||
|
||||
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('```{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')
|
||||
|
||||
|
||||
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')
|
||||
|
||||
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, ['*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',
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
```{toctree}
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
api/index
|
||||
repo
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
readme
|
||||
modules
|
||||
```
|
||||
|
||||
```{include} ../../README.md
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
```{include} ../../README.md
|
||||
```
|
|
@ -1,3 +0,0 @@
|
|||
# Code repository
|
||||
|
||||
Code repository is on GitHub: [github.com/Nonannet/pyhoff](https://github.com/Nonannet/pyhoff).
|
|
@ -1,22 +1,21 @@
|
|||
[project]
|
||||
name = "pyhoff"
|
||||
version = "1.1.2"
|
||||
version = "1.1.0"
|
||||
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"]
|
||||
|
@ -25,19 +24,10 @@ 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"]
|
||||
|
|
|
@ -137,10 +137,9 @@ 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 or provided error_value if read failed.
|
||||
The read word value.
|
||||
|
||||
Raises:
|
||||
Exception: If the word offset or count is out of range.
|
||||
|
@ -175,7 +174,6 @@ 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.
|
||||
|
@ -227,10 +225,19 @@ 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 (list[BusTerminal]): A list of bus terminal classes according to the
|
||||
bus_terminals: A list of bus terminal classes according to the
|
||||
connected terminals.
|
||||
modbus (SimpleModbusClient): The underlying modbus client used for the connection.
|
||||
modbus: The underlying modbus client used for the connection.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, port: int = 502, bus_terminals: Iterable[type[BusTerminal]] = [],
|
||||
|
@ -241,8 +248,6 @@ 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
|
||||
|
|
|
@ -224,7 +224,7 @@ class KL3054(AnalogInputTerminal):
|
|||
channel: The channel number to read from.
|
||||
|
||||
Returns:
|
||||
The current value in mA.
|
||||
The current value.
|
||||
"""
|
||||
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 in mA.
|
||||
The current value.
|
||||
"""
|
||||
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 in V.
|
||||
value: The voltage value to set.
|
||||
|
||||
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 in V.
|
||||
value: The voltage value to set.
|
||||
|
||||
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 in V.
|
||||
value: The voltage value to set.
|
||||
|
||||
Returns:
|
||||
True if the write operation succeeded.
|
||||
|
|
|
@ -48,13 +48,20 @@ 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 (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
|
||||
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
|
||||
|
||||
"""
|
||||
|
||||
|
|
Loading…
Reference in New Issue