Merge tag '2.7.0' into pkg/debian

Release 2.7.0
This commit is contained in:
2023-11-24 09:48:15 +01:00
18 changed files with 1953 additions and 1250 deletions

53
.gitignore vendored
View File

@@ -21,6 +21,7 @@ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
@@ -46,8 +47,10 @@ htmlcov/
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*.cover *.cover
*.py,cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
cover/
# Translations # Translations
*.mo *.mo
@@ -57,6 +60,7 @@ coverage.xml
*.log *.log
local_settings.py local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal
# Flask stuff: # Flask stuff:
instance/ instance/
@@ -69,6 +73,7 @@ instance/
docs/_build/ docs/_build/
# PyBuilder # PyBuilder
.pybuilder/
target/ target/
# Jupyter Notebook # Jupyter Notebook
@@ -79,10 +84,38 @@ profile_default/
ipython_config.py ipython_config.py
# pyenv # pyenv
.python-version # For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# celery beat schedule file # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule celerybeat-schedule
celerybeat.pid
# SageMath parsed files # SageMath parsed files
*.sage.py *.sage.py
@@ -114,5 +147,17 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
/make.conf # pytype static type analyzer
/test_local/ .pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# .idea/
tests_local/

View File

@@ -1,6 +1,6 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" /> <option name="LINE_SEPARATOR" value="&#10;" />
<option name="RIGHT_MARGIN" value="80" /> <option name="RIGHT_MARGIN" value="100" />
</code_scheme> </code_scheme>
</component> </component>

6
.idea/misc.xml generated
View File

@@ -1,5 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Black">
<option name="enabledOnReformat" value="true" />
<option name="executionMode" value="BINARY" />
<option name="pathToExecutable" value="/opt/homebrew/bin/black" />
<option name="sdkName" value="Python 3.11 (revpimodio2)" />
</component>
<component name="JavaScriptSettings"> <component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" /> <option name="languageLevel" value="ES6" />
</component> </component>

View File

@@ -1,221 +1,397 @@
GNU GENERAL PUBLIC LICENSE GNU LESSER GENERAL PUBLIC LICENSE
Version 2, June 1991 Version 2.1, February 1999
Copyright (C) 1989, 1991 Free Software Foundation, Inc., Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed. of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble Preamble
The licenses for most software are designed to take away your The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free Licenses are intended to guarantee your freedom to share and change
software--to make sure the software is free for all its users. This free software--to make sure the software is free for all its users.
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not This license, the Lesser General Public License, applies to some
price. Our General Public Licenses are designed to make sure that you specially designated software packages--typically libraries--of the
have the freedom to distribute copies of free software (and charge for Free Software Foundation and other authors who decide to use it. You
this service if you wish), that you receive source code or can get it can use it too, but we suggest you first think carefully about whether
if you want it, that you can change the software or use pieces of it this license or the ordinary General Public License is the better
in new free programs; and that you know you can do these things. strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights. distributors to deny you these rights or to ask you to surrender these
These restrictions translate to certain responsibilities for you if you rights. These restrictions translate to certain responsibilities for
distribute copies of the software, or if you modify it. you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of such a program, whether For example, if you distribute copies of the library, whether gratis
gratis or for a fee, you must give the recipients all the rights that or for a fee, you must give the recipients all the rights that we gave
you have. You must make sure that they, too, receive or can get the you. You must make sure that they, too, receive or can get the source
source code. And you must show them these terms so they know their code. If you link other code with the library, you must provide
rights. complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with two steps: (1) copyright the software, and We protect your rights with a two-step method: (1) we copyright the
(2) offer you this license which gives you legal permission to copy, library, and (2) we offer you this license, which gives you legal
distribute and/or modify the software. permission to copy, distribute and/or modify the library.
Also, for each author's protection and ours, we want to make certain To protect each distributor, we want to make it very clear that
that everyone understands that there is no warranty for this free there is no warranty for the free library. Also, if the library is
software. If the software is modified by someone else and passed on, we modified by someone else and passed on, the recipients should know
want its recipients to know that what they have is not the original, so that what they have is not the original version, so that the original
that any problems introduced by others will not reflect on the original author's reputation will not be affected by problems that might be
authors' reputations. introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Finally, any free program is threatened constantly by software Most GNU software, including some libraries, is covered by the
patents. We wish to avoid the danger that redistributors of a free ordinary GNU General Public License. This license, the GNU Lesser
program will individually obtain patent licenses, in effect making the General Public License, applies to certain designated libraries, and
program proprietary. To prevent this, we have made it clear that any is quite different from the ordinary General Public License. We use
patent must be licensed for everyone's free use or not licensed at all. this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and The precise terms and conditions for copying, distribution and
modification follow. modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
GNU GENERAL PUBLIC LICENSE former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains 0. This License Agreement applies to any software library or other
a notice placed by the copyright holder saying it may be distributed program which contains a notice placed by the copyright holder or
under the terms of this General Public License. The "Program", below, other authorized party saying it may be distributed under the terms of
refers to any such program or work, and a "work based on the Program" this Lesser General Public License (also called "this License").
means either the Program or any derivative work under copyright law: Each licensee is addressed as "you".
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another A "library" means a collection of software functions and/or data
language. (Hereinafter, translation is included without limitation in prepared so as to be conveniently linked with application programs
the term "modification".) Each licensee is addressed as "you". (which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program running a program using the Library is not restricted, and output from
is covered only if its contents constitute a work based on the such a program is covered only if its contents constitute a work based
Program (independent of having been made by running the Program). on the Library (independent of the use of the Library in a tool for
Whether that is true depends on what the Program does. writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Program's 1. You may copy and distribute verbatim copies of the Library's
source code as you receive it, in any medium, provided that you complete source code as you receive it, in any medium, provided that
conspicuously and appropriately publish on each copy an appropriate you conspicuously and appropriately publish on each copy an
copyright notice and disclaimer of warranty; keep intact all the appropriate copyright notice and disclaimer of warranty; keep intact
notices that refer to this License and to the absence of any warranty; all the notices that refer to this License and to the absence of any
and give any other recipients of the Program a copy of this License warranty; and distribute a copy of this License along with the
along with the Program. Library.
You may charge a fee for the physical act of transferring a copy, and You may charge a fee for the physical act of transferring a copy,
you may at your option offer warranty protection in exchange for a fee. and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and 2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1 distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions: above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change. stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in c) You must cause the whole of the work to be licensed at no
whole or in part contains or is derived from the Program or any charge to all third parties under the terms of this License.
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively d) If a facility in the modified Library refers to a function or a
when run, you must cause it, when started running for such table of data to be supplied by an application program that uses
interactive use in the most ordinary way, to print or display an the facility, other than as an argument passed when the facility
announcement including an appropriate copyright notice and a is invoked, then you must make a good faith effort to ensure that,
notice that there is no warranty (or else, saying that you provide in the event an application does not supply such function or
a warranty) and that users may redistribute the program under table, the facility still operates, and performs whatever part of
these conditions, and telling the user how to view a copy of this its purpose remains meaningful.
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on (For example, a function in a library to compute square roots has
the Program is not required to print an announcement.) a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program, identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it. entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or exercise the right to control the distribution of derivative or
collective works based on the Program. collective works based on the Library.
In addition, mere aggregation of another work not based on the Program In addition, mere aggregation of another work not based on the Library
with the Program (or with a work based on the Program) on a volume of with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under a storage or distribution medium does not bring the other work under
the scope of this License. the scope of this License.
3. You may copy and distribute the Program (or a work based on it, 3. You may opt to apply the terms of the ordinary GNU General Public
under Section 2) in object code or executable form under the terms of License instead of this License to a given copy of the Library. To do
Sections 1 and 2 above provided that you also do one of the following: this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
a) Accompany it with the complete corresponding machine-readable This option is useful when you wish to copy part of the code of
source code, which must be distributed under the terms of Sections the Library into a program that is not a library.
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three 4. You may copy and distribute the Library (or a portion or
years, to give any third party, for a charge no more than your derivative of it, under Section 2) in object code or executable form
cost of physically performing source distribution, a complete under the terms of Sections 1 and 2 above provided that you accompany
machine-readable copy of the corresponding source code, to be it with the complete corresponding machine-readable source code, which
distributed under the terms of Sections 1 and 2 above on a medium must be distributed under the terms of Sections 1 and 2 above on a
customarily used for software interchange; or, medium customarily used for software interchange.
c) Accompany it with the information you received as to the offer If distribution of object code is made by offering access to copy
to distribute corresponding source code. (This alternative is from a designated place, then offering equivalent access to copy the
allowed only for noncommercial distribution and only if you source code from the same place satisfies the requirement to
received the program in object code or executable form with such distribute the source code, even though third parties are not
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code. compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program 5. A program that contains no derivative of any portion of the
except as expressly provided under this License. Any attempt Library, but is designed to work with the Library by being compiled or
otherwise to copy, modify, sublicense or distribute the Program is linked with it, is called a "work that uses the Library". Such a
void, and will automatically terminate your rights under this License. work, in isolation, is not a derivative work of the Library, and
However, parties who have received copies, or rights, from you under therefore falls outside the scope of this License.
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the modifying or distributing the Library (or any work based on the
Program), you indicate your acceptance of this License to do so, and Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying all its terms and conditions for copying, distributing or modifying
the Program or works based on it. the Library or works based on it.
6. Each time you redistribute the Program (or any work based on the 10. Each time you redistribute the Library (or any work based on the
Program), the recipient automatically receives a license from the Library), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to original licensor to copy, distribute, link with or modify the Library
these terms and conditions. You may not impose any further subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein. restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to You are not responsible for enforcing compliance by third parties with
this License. this License.
7. If, as a consequence of a court judgment or allegation of patent 11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues), infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program. refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under If any portion of this section is held invalid or unenforceable under any
any particular circumstance, the balance of the section is intended to particular circumstance, the balance of the section is intended to apply,
apply and the section as a whole is intended to apply in other and the section as a whole is intended to apply in other circumstances.
circumstances.
It is not the purpose of this section to induce you to infringe any It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is integrity of the free software distribution system which is
implemented by public license practices. Many people have made implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that through that system in reliance on consistent application of that
@@ -226,114 +402,101 @@ impose that choice.
This section is intended to make thoroughly clear what is believed to This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License. be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in 12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License original copyright holder who places the Library under this License may add
may add an explicit geographical distribution limitation excluding an explicit geographical distribution limitation excluding those countries,
those countries, so that distribution is permitted only in or among so that distribution is permitted only in or among countries not thus
countries not thus excluded. In such case, this License incorporates excluded. In such case, this License incorporates the limitation as if
the limitation as if written in the body of this License. written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions 13. The Free Software Foundation may publish revised and/or new
of the General Public License from time to time. Such new versions will versions of the Lesser General Public License from time to time.
be similar in spirit to the present version, but may differ in detail to Such new versions will be similar in spirit to the present version,
address new problems or concerns. but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and "any specifies a version number of this License which applies to it and
later version", you have the option of following the terms and conditions "any later version", you have the option of following the terms and
either of that version or of any later version published by the Free conditions either of that version or of any later version published by
Software Foundation. If the Program does not specify a version number of the Free Software Foundation. If the Library does not specify a
this License, you may choose any version ever published by the Free Software license version number, you may choose any version ever published by
Foundation. the Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free 14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are different, write to the author programs whose distribution conditions are incompatible with these,
to ask for permission. For software which is copyrighted by the Free write to the author to ask for permission. For software which is
Software Foundation, write to the Free Software Foundation; we sometimes copyrighted by the Free Software Foundation, write to the Free
make exceptions for this. Our decision will be guided by the two goals Software Foundation; we sometimes make exceptions for this. Our
of preserving the free status of all derivatives of our free software and decision will be guided by the two goals of preserving the free status
of promoting the sharing and reuse of software generally. of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
REPAIR OR CORRECTION. THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
POSSIBILITY OF SUCH DAMAGES. SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
How to Apply These Terms to Your New Programs If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
If you develop a new program, and you want it to be of the greatest To apply these terms, attach the following notices to the library. It is
possible use to the public, the best way to achieve this is to make it safest to attach them to the start of each source file to most effectively
free software which everyone can redistribute and change under these terms. convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
To do so, attach the following notices to the program. It is safest <one line to give the library's name and a brief idea of what it does.>
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author> Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify This library is free software; you can redistribute it and/or
it under the terms of the GNU General Public License as published by modify it under the terms of the GNU Lesser General Public
the Free Software Foundation; either version 2 of the License, or License as published by the Free Software Foundation; either
(at your option) any later version. version 2.1 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
GNU General Public License for more details. Lesser General Public License for more details.
You should have received a copy of the GNU General Public License along You should have received a copy of the GNU Lesser General Public
with this program; if not, write to the Free Software Foundation, Inc., License along with this library; if not, write to the Free Software
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail. Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names: necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program Yoyodyne, Inc., hereby disclaims all copyright interest in the
`Gnomovision' (which makes passes at compilers) written by James Hacker. library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1989 <signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice Ty Coon, President of Vice
This General Public License does not permit incorporating your program into That's all there is to it!
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -6,3 +6,4 @@ include MANIFEST.in
include README.md include README.md
include requirements.txt include requirements.txt
include setup.py include setup.py
recursive-include tests *

View File

@@ -26,32 +26,56 @@ venv-info:
exit 0 exit 0
venv: venv:
$(SYSTEM_PYTHON) -m venv "$(VENV_PATH)" # Start with empty environment
source $(VENV_PATH)/bin/activate && \ "$(SYSTEM_PYTHON)" -m venv "$(VENV_PATH)"
source "$(VENV_PATH)/bin/activate" && \
python3 -m pip install --upgrade pip && \ python3 -m pip install --upgrade pip && \
python3 -m pip install -r requirements.txt python3 -m pip install -r requirements.txt
exit 0 exit 0
.PHONY: venv-info venv venv-ssp:
# Include system installed site-packages and add just missing modules
"$(SYSTEM_PYTHON)" -m venv --system-site-packages "$(VENV_PATH)"
source "$(VENV_PATH)/bin/activate" && \
python3 -m pip install --upgrade pip && \
python3 -m pip install -r requirements.txt
exit 0
.PHONY: venv-info venv venv-ssp
## Build steps
test:
PYTHONPATH=src "$(PYTHON)" -m pytest tests
## Build, install
build: build:
$(PYTHON) -m setup sdist "$(PYTHON)" -m setup sdist
$(PYTHON) -m setup bdist_wheel "$(PYTHON)" -m setup bdist_wheel
install: build install: build
$(PYTHON) -m pip install dist/$(PACKAGE)-*.whl "$(PYTHON)" -m pip install dist/$(PACKAGE)-$(APP_VERSION)-*.whl
uninstall:
"$(PYTHON)" -m pip uninstall --yes $(PACKAGE)
.PHONY: test build install uninstall
## Documentation
docs: docs:
$(PYTHON) -m sphinx.cmd.build -b html docs docs/_build/html "$(PYTHON)" -m sphinx.cmd.build -b html docs docs/_build/html
.PHONY: build docs install .PHONY: docs
## Clean ## Clean
clean: clean:
rm -rf build docs/_build dist src/*.egg-info *.spec # PyTest caches
rm -rf .pytest_cache
# Build artifacts
rm -rf build dist src/*.egg-info
# PyInstaller created files
rm -rf *.spec
clean-all: clean distclean: clean
rm -R $(VENV_PATH) # Virtual environment
rm -rf "$(VENV_PATH)"
.PHONY: clean clean-all .PHONY: clean distclean

2
pyproject.toml Normal file
View File

@@ -0,0 +1,2 @@
[tool.black]
line-length = 100

View File

@@ -1,3 +1,7 @@
setuptools>=58.0.4 # Build dependencies
wheel pytest-cov
setuptools
sphinx sphinx
wheel
# Runtime dependencies, must match install_requires in setup.py

View File

@@ -3,4 +3,4 @@
__author__ = "Sven Sager <akira@revpimodio.org>" __author__ = "Sven Sager <akira@revpimodio.org>"
__copyright__ = "Copyright (C) 2023 Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "LGPLv2" __license__ = "LGPLv2"
__version__ = "2.6.1" __version__ = "2.7.0"

View File

@@ -14,10 +14,25 @@ fuehrt das Modul bei Datenaenderung aus.
""" """
__all__ = [ __all__ = [
"IOEvent", "IOEvent",
"RevPiModIO", "RevPiModIODriver", "RevPiModIOSelected", "run_plc", "RevPiModIO",
"RevPiNetIO", "RevPiNetIODriver", "RevPiNetIOSelected", "run_net_plc", "RevPiModIODriver",
"Cycletools", "EventCallback", "RevPiModIOSelected",
"ProductType", "AIO", "COMPACT", "DI", "DO", "DIO", "FLAT", "MIO", "run_plc",
"RevPiNetIO",
"RevPiNetIODriver",
"RevPiNetIOSelected",
"run_net_plc",
"Cycletools",
"EventCallback",
"ProductType",
"DeviceType",
"AIO",
"COMPACT",
"DI",
"DO",
"DIO",
"FLAT",
"MIO",
] ]
__author__ = "Sven Sager <akira@revpimodio.org>" __author__ = "Sven Sager <akira@revpimodio.org>"
__copyright__ = "Copyright (C) 2023 Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager"
@@ -29,4 +44,4 @@ from .helper import Cycletools, EventCallback
from .io import IOEvent from .io import IOEvent
from .modio import RevPiModIO, RevPiModIODriver, RevPiModIOSelected, run_plc from .modio import RevPiModIO, RevPiModIODriver, RevPiModIOSelected, run_plc
from .netio import RevPiNetIO, RevPiNetIODriver, RevPiNetIOSelected, run_net_plc from .netio import RevPiNetIO, RevPiNetIODriver, RevPiNetIOSelected, run_net_plc
from .pictory import ProductType, AIO, COMPACT, DI, DO, DIO, FLAT, MIO from .pictory import ProductType, DeviceType, AIO, COMPACT, DI, DO, DIO, FLAT, MIO

View File

@@ -29,11 +29,11 @@ def acheck(check_type, **kwargs) -> None:
for var_name in kwargs: for var_name in kwargs:
none_okay = var_name.endswith("_noneok") none_okay = var_name.endswith("_noneok")
if not (isinstance(kwargs[var_name], check_type) or if not (isinstance(kwargs[var_name], check_type) or none_okay and kwargs[var_name] is None):
none_okay and kwargs[var_name] is None):
msg = "Argument '{0}' must be {1}{2}".format( msg = "Argument '{0}' must be {1}{2}".format(
var_name.rstrip("_noneok"), str(check_type), var_name.rstrip("_noneok"),
" or <class 'NoneType'>" if none_okay else "" str(check_type),
" or <class 'NoneType'>" if none_okay else "",
) )
raise TypeError(msg) raise TypeError(msg)

File diff suppressed because it is too large Load Diff

View File

@@ -87,12 +87,31 @@ class Cycletools:
Lampen synchron blinken zu lassen. Lampen synchron blinken zu lassen.
""" """
__slots__ = "__cycle", "__cycletime", "__ucycle", "__dict_ton", \ __slots__ = (
"__dict_tof", "__dict_tp", "__dict_change", \ "__cycle",
"_start_timer", "core", "device", \ "__cycletime",
"first", "io", "last", "var", \ "__ucycle",
"flag1c", "flag5c", "flag10c", "flag15c", "flag20c", \ "__dict_ton",
"flank5c", "flank10c", "flank15c", "flank20c" "__dict_tof",
"__dict_tp",
"__dict_change",
"_start_timer",
"core",
"device",
"first",
"io",
"last",
"var",
"flag1c",
"flag5c",
"flag10c",
"flag15c",
"flag20c",
"flank5c",
"flank10c",
"flank15c",
"flank20c",
)
def __init__(self, cycletime, revpi_object): def __init__(self, cycletime, revpi_object):
"""Init Cycletools class.""" """Init Cycletools class."""
@@ -208,16 +227,13 @@ class Cycletools:
else: else:
value = io.get_value() value = io.get_value()
return self.__dict_change[io] != value and ( return self.__dict_change[io] != value and (
value and edge == RISING or value and edge == RISING or not value and edge == FALLING
not value and edge == FALLING
) )
else: else:
if not isinstance(io, IOBase): if not isinstance(io, IOBase):
raise TypeError("parameter 'io' must be an io object") raise TypeError("parameter 'io' must be an io object")
if not (edge == BOTH or type(io.value) == bool): if not (edge == BOTH or type(io.value) == bool):
raise ValueError( raise ValueError("parameter 'edge' can be used with bit io objects only")
"parameter 'edge' can be used with bit io objects only"
)
self.__dict_change[io] = None self.__dict_change[io] = None
return False return False
@@ -283,8 +299,7 @@ class Cycletools:
:param milliseconds: Millisekunden, der Verzoegerung wenn neu gestartet :param milliseconds: Millisekunden, der Verzoegerung wenn neu gestartet
""" """
if self.__dict_ton.get(name, [-1])[0] == -1: if self.__dict_ton.get(name, [-1])[0] == -1:
self.__dict_ton[name] = \ self.__dict_ton[name] = [ceil(milliseconds / self.__cycletime), True]
[ceil(milliseconds / self.__cycletime), True]
else: else:
self.__dict_ton[name][1] = True self.__dict_ton[name][1] = True
@@ -326,8 +341,7 @@ class Cycletools:
:param milliseconds: Millisekunden, die der Impuls anstehen soll :param milliseconds: Millisekunden, die der Impuls anstehen soll
""" """
if self.__dict_tp.get(name, [-1])[0] == -1: if self.__dict_tp.get(name, [-1])[0] == -1:
self.__dict_tp[name] = \ self.__dict_tp[name] = [ceil(milliseconds / self.__cycletime), True]
[ceil(milliseconds / self.__cycletime), True]
else: else:
self.__dict_tp[name][1] = True self.__dict_tp[name][1] = True
@@ -364,9 +378,19 @@ class ProcimgWriter(Thread):
Event-Handling verwendet. Event-Handling verwendet.
""" """
__slots__ = "__dict_delay", "__eventth", "_eventqth", "__eventwork", \ __slots__ = (
"_eventq", "_modio", \ "__dict_delay",
"_refresh", "_work", "daemon", "lck_refresh", "newdata" "__eventth",
"_eventqth",
"__eventwork",
"_eventq",
"_modio",
"_refresh",
"_work",
"daemon",
"lck_refresh",
"newdata",
)
def __init__(self, parentmodio): def __init__(self, parentmodio):
"""Init ProcimgWriter class.""" """Init ProcimgWriter class."""
@@ -387,43 +411,38 @@ class ProcimgWriter(Thread):
def __check_change(self, dev) -> None: def __check_change(self, dev) -> None:
"""Findet Aenderungen fuer die Eventueberwachung.""" """Findet Aenderungen fuer die Eventueberwachung."""
for io_event in dev._dict_events: for io_event in dev._dict_events:
if dev._ba_datacp[io_event._slc_address] == dev._ba_devdata[io_event._slc_address]:
if dev._ba_datacp[io_event._slc_address] == \
dev._ba_devdata[io_event._slc_address]:
continue continue
if io_event._bitshift: if io_event._bitshift:
boolcp = dev._ba_datacp[io_event._slc_address.start] \ boolcp = dev._ba_datacp[io_event._slc_address.start] & io_event._bitshift
& io_event._bitshift boolor = dev._ba_devdata[io_event._slc_address.start] & io_event._bitshift
boolor = dev._ba_devdata[io_event._slc_address.start] \
& io_event._bitshift
if boolor == boolcp: if boolor == boolcp:
continue continue
for regfunc in dev._dict_events[io_event]: for regfunc in dev._dict_events[io_event]:
if regfunc.edge == BOTH \ if (
or regfunc.edge == RISING and boolor \ regfunc.edge == BOTH
or regfunc.edge == FALLING and not boolor: or regfunc.edge == RISING
and boolor
or regfunc.edge == FALLING
and not boolor
):
if regfunc.delay == 0: if regfunc.delay == 0:
if regfunc.as_thread: if regfunc.as_thread:
self._eventqth.put( self._eventqth.put((regfunc, io_event._name, io_event.value), False)
(regfunc, io_event._name, io_event.value),
False
)
else: else:
self._eventq.put( self._eventq.put((regfunc, io_event._name, io_event.value), False)
(regfunc, io_event._name, io_event.value),
False
)
else: else:
# Verzögertes Event in dict einfügen # Verzögertes Event in dict einfügen
tup_fire = ( tup_fire = (
regfunc, io_event._name, io_event.value, regfunc,
io_event._name,
io_event.value,
io_event, io_event,
) )
if regfunc.overwrite \ if regfunc.overwrite or tup_fire not in self.__dict_delay:
or tup_fire not in self.__dict_delay:
self.__dict_delay[tup_fire] = ceil( self.__dict_delay[tup_fire] = ceil(
regfunc.delay / 1000 / self._refresh regfunc.delay / 1000 / self._refresh
) )
@@ -431,26 +450,19 @@ class ProcimgWriter(Thread):
for regfunc in dev._dict_events[io_event]: for regfunc in dev._dict_events[io_event]:
if regfunc.delay == 0: if regfunc.delay == 0:
if regfunc.as_thread: if regfunc.as_thread:
self._eventqth.put( self._eventqth.put((regfunc, io_event._name, io_event.value), False)
(regfunc, io_event._name, io_event.value),
False
)
else: else:
self._eventq.put( self._eventq.put((regfunc, io_event._name, io_event.value), False)
(regfunc, io_event._name, io_event.value),
False
)
else: else:
# Verzögertes Event in dict einfügen # Verzögertes Event in dict einfügen
tup_fire = ( tup_fire = (
regfunc, io_event._name, io_event.value, regfunc,
io_event._name,
io_event.value,
io_event, io_event,
) )
if regfunc.overwrite \ if regfunc.overwrite or tup_fire not in self.__dict_delay:
or tup_fire not in self.__dict_delay: self.__dict_delay[tup_fire] = ceil(regfunc.delay / 1000 / self._refresh)
self.__dict_delay[tup_fire] = ceil(
regfunc.delay / 1000 / self._refresh
)
# Nach Verarbeitung aller IOs die Bytes kopieren (Lock ist noch drauf) # Nach Verarbeitung aller IOs die Bytes kopieren (Lock ist noch drauf)
dev._ba_datacp = dev._ba_devdata[:] dev._ba_datacp = dev._ba_devdata[:]
@@ -460,9 +472,7 @@ class ProcimgWriter(Thread):
while self.__eventwork: while self.__eventwork:
try: try:
tup_fireth = self._eventqth.get(timeout=1) tup_fireth = self._eventqth.get(timeout=1)
th = EventCallback( th = EventCallback(tup_fireth[0].func, tup_fireth[1], tup_fireth[2])
tup_fireth[0].func, tup_fireth[1], tup_fireth[2]
)
th.start() th.start()
self._eventqth.task_done() self._eventqth.task_done()
except queue.Empty: except queue.Empty:
@@ -524,7 +534,7 @@ class ProcimgWriter(Thread):
warnings.warn( warnings.warn(
"cycle time of {0} ms exceeded in your cycle function" "cycle time of {0} ms exceeded in your cycle function"
"".format(int(self._refresh * 1000)), "".format(int(self._refresh * 1000)),
RuntimeWarning RuntimeWarning,
) )
mrk_delay = self._refresh mrk_delay = self._refresh
# Nur durch cycleloop erreichbar - keine verzögerten Events # Nur durch cycleloop erreichbar - keine verzögerten Events
@@ -545,23 +555,25 @@ class ProcimgWriter(Thread):
# Read all device bytes, because it is shared # Read all device bytes, because it is shared
fh.seek(dev.offset) fh.seek(dev.offset)
bytesbuff[dev._slc_devoff] = \ bytesbuff[dev._slc_devoff] = fh.read(len(dev._ba_devdata))
fh.read(len(dev._ba_devdata))
if self._modio._monitoring or dev._shared_procimg: if self._modio._monitoring or dev._shared_procimg:
# Inputs und Outputs in Puffer # Inputs und Outputs in Puffer
dev._ba_devdata[:] = bytesbuff[dev._slc_devoff] dev._ba_devdata[:] = bytesbuff[dev._slc_devoff]
if self.__eventwork \ if (
and len(dev._dict_events) > 0 \ self.__eventwork
and dev._ba_datacp != dev._ba_devdata: and len(dev._dict_events) > 0
and dev._ba_datacp != dev._ba_devdata
):
self.__check_change(dev) self.__check_change(dev)
else: else:
# Inputs in Puffer, Outputs in Prozessabbild # Inputs in Puffer, Outputs in Prozessabbild
dev._ba_devdata[dev._slc_inp] = \ dev._ba_devdata[dev._slc_inp] = bytesbuff[dev._slc_inpoff]
bytesbuff[dev._slc_inpoff] if (
if self.__eventwork \ self.__eventwork
and len(dev._dict_events) > 0 \ and len(dev._dict_events) > 0
and dev._ba_datacp != dev._ba_devdata: and dev._ba_datacp != dev._ba_devdata
):
self.__check_change(dev) self.__check_change(dev)
fh.seek(dev._slc_outoff.start) fh.seek(dev._slc_outoff.start)
@@ -579,16 +591,13 @@ class ProcimgWriter(Thread):
else: else:
if not mrk_warn: if not mrk_warn:
if self._modio._debug == 0: if self._modio._debug == 0:
warnings.warn( warnings.warn("recover from io errors on process image", RuntimeWarning)
"recover from io errors on process image",
RuntimeWarning
)
else: else:
warnings.warn( warnings.warn(
"recover from io errors on process image - total " "recover from io errors on process image - total "
"count of {0} errors now" "count of {0} errors now"
"".format(self._modio._ioerror), "".format(self._modio._ioerror),
RuntimeWarning RuntimeWarning,
) )
mrk_warn = True mrk_warn = True
@@ -600,8 +609,7 @@ class ProcimgWriter(Thread):
# Verzögerte Events prüfen # Verzögerte Events prüfen
if self.__eventwork: if self.__eventwork:
for tup_fire in tuple(self.__dict_delay.keys()): for tup_fire in tuple(self.__dict_delay.keys()):
if tup_fire[0].overwrite and \ if tup_fire[0].overwrite and tup_fire[3].value != tup_fire[2]:
tup_fire[3].value != tup_fire[2]:
del self.__dict_delay[tup_fire] del self.__dict_delay[tup_fire]
else: else:
self.__dict_delay[tup_fire] -= 1 self.__dict_delay[tup_fire] -= 1
@@ -617,9 +625,8 @@ class ProcimgWriter(Thread):
# Second default_timer call include calculation time from above # Second default_timer call include calculation time from above
if default_timer() - ot > self._refresh: if default_timer() - ot > self._refresh:
warnings.warn( warnings.warn(
"io refresh time of {0} ms exceeded!" "io refresh time of {0} ms exceeded!".format(int(self._refresh * 1000)),
"".format(int(self._refresh * 1000)), RuntimeWarning,
RuntimeWarning
) )
mrk_delay = 0.0 mrk_delay = 0.0
else: else:
@@ -641,8 +648,6 @@ class ProcimgWriter(Thread):
if type(value) == int and 5 <= value <= 2000: if type(value) == int and 5 <= value <= 2000:
self._refresh = value / 1000 self._refresh = value / 1000
else: else:
raise ValueError( raise ValueError("refresh time must be 5 to 2000 milliseconds")
"refresh time must be 5 to 2000 milliseconds"
)
refresh = property(get_refresh, set_refresh) refresh = property(get_refresh, set_refresh)

View File

@@ -5,11 +5,11 @@ __copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "LGPLv2" __license__ = "LGPLv2"
import struct import struct
import warnings
from re import match as rematch from re import match as rematch
from threading import Event from threading import Event
from ._internal import consttostr, RISING, FALLING, BOTH, INP, OUT, \ from ._internal import consttostr, RISING, FALLING, BOTH, INP, OUT, MEM, PROCESS_IMAGE_SIZE
MEM, PROCESS_IMAGE_SIZE
try: try:
# Funktioniert nur auf Unix # Funktioniert nur auf Unix
@@ -36,10 +36,11 @@ class IOEvent(object):
class IOList(object): class IOList(object):
"""Basisklasse fuer direkten Zugriff auf IO Objekte.""" """Basisklasse fuer direkten Zugriff auf IO Objekte."""
def __init__(self): def __init__(self, modio):
"""Init IOList class.""" """Init IOList class."""
self.__dict_iobyte = {k: [] for k in range(PROCESS_IMAGE_SIZE)} self.__dict_iobyte = {k: [] for k in range(PROCESS_IMAGE_SIZE)}
self.__dict_iorefname = {} self.__dict_iorefname = {}
self.__modio = modio
def __contains__(self, key): def __contains__(self, key):
""" """
@@ -69,8 +70,16 @@ class IOList(object):
self.__dict_iobyte[io_del.address][io_del._bitaddress] = None self.__dict_iobyte[io_del.address][io_del._bitaddress] = None
# Do not use any() because we want to know None, not 0 # Do not use any() because we want to know None, not 0
if self.__dict_iobyte[io_del.address] == \ if self.__dict_iobyte[io_del.address] == [
[None, None, None, None, None, None, None, None]: None,
None,
None,
None,
None,
None,
None,
None,
]:
self.__dict_iobyte[io_del.address] = [] self.__dict_iobyte[io_del.address] = []
else: else:
self.__dict_iobyte[io_del.address].remove(io_del) self.__dict_iobyte[io_del.address].remove(io_del)
@@ -78,6 +87,44 @@ class IOList(object):
object.__delattr__(self, key) object.__delattr__(self, key)
io_del._parentdevice._update_my_io_list() io_del._parentdevice._update_my_io_list()
def __enter__(self):
"""
Read inputs on entering context manager and write outputs on leaving.
All entries are read when entering the context manager. Within the
context manager, further .readprocimg() or .writeprocimg() calls can
be made and the process image can be read or written. When exiting,
all outputs are always written into the process image.
When 'autorefresh=True' is used, all read or write actions in the
background are performed automatically.
"""
if not self.__modio._context_manager:
# If ModIO itself is in a context manager, it sets the _looprunning=True flag itself
if self.__modio._looprunning:
raise RuntimeError("can not enter context manager inside mainloop or cycleloop")
self.__modio._looprunning = True
self.__modio.readprocimg()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Write outputs to process image before leaving the context manager."""
if self.__modio._imgwriter.is_alive():
# Reset new data flat to sync with imgwriter
self.__modio._imgwriter.newdata.clear()
# Write outputs on devices without autorefresh
self.__modio.writeprocimg()
if self.__modio._imgwriter.is_alive():
# Wait until imgwriter has written outputs
self.__modio._imgwriter.newdata.wait(2.5)
if self.__modio._context_manager:
# Do not reset if ModIO is in a context manager itself, it will handle that flag
self.__modio._looprunning = False
def __getattr__(self, key): def __getattr__(self, key):
""" """
Verwaltet geloeschte IOs (Attribute, die nicht existieren). Verwaltet geloeschte IOs (Attribute, die nicht existieren).
@@ -110,9 +157,7 @@ class IOList(object):
elif type(key) == slice: elif type(key) == slice:
return [ return [
self.__dict_iobyte[int_io] self.__dict_iobyte[int_io]
for int_io in range( for int_io in range(key.start, key.stop, 1 if key.step is None else key.step)
key.start, key.stop, 1 if key.step is None else key.step
)
] ]
else: else:
return getattr(self, key) return getattr(self, key)
@@ -145,13 +190,12 @@ class IOList(object):
"""Verbietet aus Leistungsguenden das direkte Setzen von Attributen.""" """Verbietet aus Leistungsguenden das direkte Setzen von Attributen."""
if key in ( if key in (
"_IOList__dict_iobyte", "_IOList__dict_iobyte",
"_IOList__dict_iorefname" "_IOList__dict_iorefname",
"_IOList__modio",
): ):
object.__setattr__(self, key, value) object.__setattr__(self, key, value)
else: else:
raise AttributeError( raise AttributeError("direct assignment is not supported - use .value Attribute")
"direct assignment is not supported - use .value Attribute"
)
def __private_replace_oldio_with_newio(self, io) -> None: def __private_replace_oldio_with_newio(self, io) -> None:
""" """
@@ -168,16 +212,17 @@ class IOList(object):
scan_stop = scan_start + (1 if io._length == 0 else io._length) scan_stop = scan_start + (1 if io._length == 0 else io._length)
# Defaultvalue über mehrere Bytes sammeln # Defaultvalue über mehrere Bytes sammeln
calc_defaultvalue = b'' calc_defaultvalue = b""
for i in range(scan_start, scan_stop): for i in range(scan_start, scan_stop):
for oldio in self.__dict_iobyte[i]: for oldio in self.__dict_iobyte[i]:
if type(oldio) == StructIO: if type(oldio) == StructIO:
# Hier gibt es schon einen neuen IO # Hier gibt es schon einen neuen IO
if oldio._bitshift: if oldio._bitshift:
if io._bitshift == oldio._bitshift \ if (
and io._slc_address == oldio._slc_address: io._bitshift == oldio._bitshift
and io._slc_address == oldio._slc_address
):
raise MemoryError( raise MemoryError(
"bit {0} already assigned to '{1}'".format( "bit {0} already assigned to '{1}'".format(
io._bitaddress, oldio._name io._bitaddress, oldio._name
@@ -186,9 +231,7 @@ class IOList(object):
else: else:
# Bereits überschriebene bytes sind ungültig # Bereits überschriebene bytes sind ungültig
raise MemoryError( raise MemoryError(
"new io '{0}' overlaps memory of '{1}'".format( "new io '{0}' overlaps memory of '{1}'".format(io._name, oldio._name)
io._name, oldio._name
)
) )
elif oldio is not None: elif oldio is not None:
# IOs im Speicherbereich des neuen IO merken # IOs im Speicherbereich des neuen IO merken
@@ -201,8 +244,7 @@ class IOList(object):
if io._byteorder == "little": if io._byteorder == "little":
calc_defaultvalue += oldio._defaultvalue calc_defaultvalue += oldio._defaultvalue
else: else:
calc_defaultvalue = \ calc_defaultvalue = oldio._defaultvalue + calc_defaultvalue
oldio._defaultvalue + calc_defaultvalue
# ios aus listen entfernen # ios aus listen entfernen
delattr(self, oldio._name) delattr(self, oldio._name)
@@ -211,9 +253,7 @@ class IOList(object):
# Nur bei StructIO und keiner gegebenen defaultvalue übernehmen # Nur bei StructIO und keiner gegebenen defaultvalue übernehmen
if io._bitshift: if io._bitshift:
io_byte_address = io._parentio_address - io.address io_byte_address = io._parentio_address - io.address
io._defaultvalue = bool( io._defaultvalue = bool(io._parentio_defaultvalue[io_byte_address] & io._bitshift)
io._parentio_defaultvalue[io_byte_address] & io._bitshift
)
else: else:
io._defaultvalue = calc_defaultvalue io._defaultvalue = calc_defaultvalue
@@ -226,25 +266,75 @@ class IOList(object):
if isinstance(new_io, IOBase): if isinstance(new_io, IOBase):
if hasattr(self, new_io._name): if hasattr(self, new_io._name):
raise AttributeError( raise AttributeError(
"attribute {0} already exists - can not set io" "attribute {0} already exists - can not set io".format(new_io._name)
"".format(new_io._name)
) )
if type(new_io) is StructIO: do_replace = type(new_io) is StructIO
if do_replace:
self.__private_replace_oldio_with_newio(new_io) self.__private_replace_oldio_with_newio(new_io)
object.__setattr__(self, new_io._name, new_io)
# Bytedict für Adresszugriff anpassen # Bytedict für Adresszugriff anpassen
if new_io._bitshift: if new_io._bitshift:
if len(self.__dict_iobyte[new_io.address]) != 8: if len(self.__dict_iobyte[new_io.address]) != 8:
# "schnell" 8 Einträge erstellen da es BIT IOs sind # "schnell" 8 Einträge erstellen da es BIT IOs sind
self.__dict_iobyte[new_io.address] += \ self.__dict_iobyte[new_io.address] += [
[None, None, None, None, None, None, None, None] None,
None,
None,
None,
None,
None,
None,
None,
]
# Check for overlapping IOs
if (
not do_replace
and self.__dict_iobyte[new_io.address][new_io._bitaddress] is not None
):
warnings.warn(
"ignore io '{0}', as an io already exists at the address '{1} Bit {2}'. "
"this can be caused by an incorrect pictory configuration.".format(
new_io.name,
new_io.address,
new_io._bitaddress,
),
Warning,
)
return
self.__dict_iobyte[new_io.address][new_io._bitaddress] = new_io self.__dict_iobyte[new_io.address][new_io._bitaddress] = new_io
else: else:
# Search the previous IO to calculate the length
offset_end = new_io.address
search_index = new_io.address
while search_index >= 0:
previous_io = self.__dict_iobyte[search_index]
if len(previous_io) == 8:
# Bits on this address are always 1 byte
offset_end -= 1
elif len(previous_io) == 1:
# Found IO, calculate offset + length of IO
offset_end = previous_io[0].address + previous_io[0].length
break
search_index -= 1
# Check if the length of the previous IO overlaps with the new IO
if offset_end > new_io.address:
warnings.warn(
"ignore io '{0}', as an io already exists at the address '{1}'. "
"this can be caused by an incorrect pictory configuration.".format(
new_io.name,
new_io.address,
),
Warning,
)
return
self.__dict_iobyte[new_io.address].append(new_io) self.__dict_iobyte[new_io.address].append(new_io)
object.__setattr__(self, new_io._name, new_io)
if type(new_io) is StructIO: if type(new_io) is StructIO:
new_io._parentdevice._update_my_io_list() new_io._parentdevice._update_my_io_list()
else: else:
@@ -289,13 +379,26 @@ class IOBase(object):
auch als <class 'int'> verwendet werden koennen. auch als <class 'int'> verwendet werden koennen.
""" """
__slots__ = "__bit_ioctl_off", "__bit_ioctl_on", "_bitaddress", \ __slots__ = (
"_bitshift", "_bitlength", "_byteorder", "_defaultvalue", \ "__bit_ioctl_off",
"_export", "_iotype", "_length", "_name", "_parentdevice", \ "__bit_ioctl_on",
"_read_only_io", "_signed", "_slc_address", "bmk" "_bitaddress",
"_bitshift",
"_bitlength",
"_byteorder",
"_defaultvalue",
"_export",
"_iotype",
"_length",
"_name",
"_parentdevice",
"_read_only_io",
"_signed",
"_slc_address",
"bmk",
)
def __init__(self, parentdevice, valuelist: list, iotype: int, def __init__(self, parentdevice, valuelist: list, iotype: int, byteorder: str, signed: bool):
byteorder: str, signed: bool):
""" """
Instantiierung der IOBase-Klasse. Instantiierung der IOBase-Klasse.
@@ -312,8 +415,7 @@ class IOBase(object):
# Bitadressen auf Bytes aufbrechen und umrechnen # Bitadressen auf Bytes aufbrechen und umrechnen
self._bitaddress = -1 if valuelist[7] == "" else int(valuelist[7]) % 8 self._bitaddress = -1 if valuelist[7] == "" else int(valuelist[7]) % 8
self._bitshift = None if self._bitaddress == -1 \ self._bitshift = None if self._bitaddress == -1 else 1 << self._bitaddress
else 1 << self._bitaddress
# Längenberechnung # Längenberechnung
self._bitlength = int(valuelist[2]) self._bitlength = int(valuelist[2])
@@ -333,9 +435,7 @@ class IOBase(object):
if self._bitshift: if self._bitshift:
# Höhere Bits als 7 auf nächste Bytes umbrechen # Höhere Bits als 7 auf nächste Bytes umbrechen
int_startaddress += int(int(valuelist[7]) / 8) int_startaddress += int(int(valuelist[7]) / 8)
self._slc_address = slice( self._slc_address = slice(int_startaddress, int_startaddress + 1)
int_startaddress, int_startaddress + 1
)
# Defaultvalue ermitteln, sonst False # Defaultvalue ermitteln, sonst False
if valuelist[1] is None and type(self) == StructIO: if valuelist[1] is None and type(self) == StructIO:
@@ -347,14 +447,10 @@ class IOBase(object):
self._defaultvalue = False self._defaultvalue = False
# Ioctl für Bitsetzung setzen # Ioctl für Bitsetzung setzen
self.__bit_ioctl_off = struct.pack( self.__bit_ioctl_off = struct.pack("<HB", self._get_address(), self._bitaddress)
"<HB", self._get_address(), self._bitaddress self.__bit_ioctl_on = self.__bit_ioctl_off + b"\x01"
)
self.__bit_ioctl_on = self.__bit_ioctl_off + b'\x01'
else: else:
self._slc_address = slice( self._slc_address = slice(int_startaddress, int_startaddress + self._length)
int_startaddress, int_startaddress + self._length
)
if str(valuelist[1]).isdigit(): if str(valuelist[1]).isdigit():
# Defaultvalue aus Zahl in Bytes umrechnen # Defaultvalue aus Zahl in Bytes umrechnen
self._defaultvalue = int(valuelist[1]).to_bytes( self._defaultvalue = int(valuelist[1]).to_bytes(
@@ -382,8 +478,7 @@ class IOBase(object):
try: try:
buff = valuelist[1].encode("ASCII") buff = valuelist[1].encode("ASCII")
if len(buff) <= self._length: if len(buff) <= self._length:
self._defaultvalue = \ self._defaultvalue = buff + bytes(self._length - len(buff))
buff + bytes(self._length - len(buff))
except Exception: except Exception:
pass pass
@@ -394,10 +489,7 @@ class IOBase(object):
:return: <class 'bool'> Nur False wenn False oder 0 sonst True :return: <class 'bool'> Nur False wenn False oder 0 sonst True
""" """
if self._bitshift: if self._bitshift:
return bool( return bool(self._parentdevice._ba_devdata[self._slc_address.start] & self._bitshift)
self._parentdevice._ba_devdata[self._slc_address.start]
& self._bitshift
)
else: else:
return any(self._parentdevice._ba_devdata[self._slc_address]) return any(self._parentdevice._ba_devdata[self._slc_address])
@@ -406,8 +498,7 @@ class IOBase(object):
# Inline get_value() # Inline get_value()
if self._bitshift: if self._bitshift:
return bool( return bool(
self._parentdevice._ba_devdata[self._slc_address.start] self._parentdevice._ba_devdata[self._slc_address.start] & self._bitshift
& self._bitshift
) )
else: else:
return bytes(self._parentdevice._ba_devdata[self._slc_address]) return bytes(self._parentdevice._ba_devdata[self._slc_address])
@@ -430,8 +521,9 @@ class IOBase(object):
""" """
return self._name return self._name
def __reg_xevent(self, func, delay: int, edge: int, as_thread: bool, def __reg_xevent(
overwrite: bool, prefire: bool) -> None: self, func, delay: int, edge: int, as_thread: bool, overwrite: bool, prefire: bool
) -> None:
""" """
Verwaltet reg_event und reg_timerevent. Verwaltet reg_event und reg_timerevent.
@@ -444,26 +536,19 @@ class IOBase(object):
""" """
# Prüfen ob Funktion callable ist # Prüfen ob Funktion callable ist
if not callable(func): if not callable(func):
raise ValueError( raise ValueError("registered function '{0}' is not callable".format(func))
"registered function '{0}' is not callable".format(func)
)
if type(delay) != int or delay < 0: if type(delay) != int or delay < 0:
raise ValueError( raise ValueError("'delay' must be <class 'int'> and greater or equal 0")
"'delay' must be <class 'int'> and greater or equal 0"
)
if edge != BOTH and not self._bitshift: if edge != BOTH and not self._bitshift:
raise ValueError( raise ValueError("parameter 'edge' can be used with bit io objects only")
"parameter 'edge' can be used with bit io objects only"
)
if prefire and self._parentdevice._modio._looprunning: if prefire and self._parentdevice._modio._looprunning:
raise RuntimeError( raise RuntimeError("prefire can not be used if mainloop is running")
"prefire can not be used if mainloop is running"
)
if self not in self._parentdevice._dict_events: if self not in self._parentdevice._dict_events:
with self._parentdevice._filelock: with self._parentdevice._filelock:
self._parentdevice._dict_events[self] = \ self._parentdevice._dict_events[self] = [
[IOEvent(func, edge, as_thread, delay, overwrite, prefire)] IOEvent(func, edge, as_thread, delay, overwrite, prefire)
]
else: else:
# Prüfen ob Funktion schon registriert ist # Prüfen ob Funktion schon registriert ist
for regfunc in self._parentdevice._dict_events[self]: for regfunc in self._parentdevice._dict_events[self]:
@@ -476,10 +561,7 @@ class IOBase(object):
raise RuntimeError( raise RuntimeError(
"io '{0}' with function '{1}' already in list " "io '{0}' with function '{1}' already in list "
"with edge '{2}' - edge '{3}' not allowed anymore" "with edge '{2}' - edge '{3}' not allowed anymore"
"".format( "".format(self._name, func, consttostr(regfunc.edge), consttostr(edge))
self._name, func,
consttostr(regfunc.edge), consttostr(edge)
)
) )
else: else:
raise RuntimeError( raise RuntimeError(
@@ -490,9 +572,7 @@ class IOBase(object):
elif regfunc.edge == edge: elif regfunc.edge == edge:
raise RuntimeError( raise RuntimeError(
"io '{0}' with function '{1}' for given edge '{2}' " "io '{0}' with function '{1}' for given edge '{2}' "
"already in list".format( "already in list".format(self._name, func, consttostr(edge))
self._name, func, consttostr(edge)
)
) )
# Eventfunktion einfügen # Eventfunktion einfügen
@@ -548,9 +628,7 @@ class IOBase(object):
if self._bitshift: if self._bitshift:
# Write single bit to process image # Write single bit to process image
value = \ value = self._parentdevice._ba_devdata[self._slc_address.start] & self._bitshift
self._parentdevice._ba_devdata[self._slc_address.start] & \
self._bitshift
if self._parentdevice._modio._run_on_pi: if self._parentdevice._modio._run_on_pi:
# IOCTL auf dem RevPi # IOCTL auf dem RevPi
with self._parentdevice._modio._myfh_lck: with self._parentdevice._modio._myfh_lck:
@@ -559,8 +637,7 @@ class IOBase(object):
ioctl( ioctl(
self._parentdevice._modio._myfh, self._parentdevice._modio._myfh,
19216, 19216,
self.__bit_ioctl_on if value self.__bit_ioctl_on if value else self.__bit_ioctl_off,
else self.__bit_ioctl_off
) )
except Exception as e: except Exception as e:
self._parentdevice._modio._gotioerror("ioset", e) self._parentdevice._modio._gotioerror("ioset", e)
@@ -572,12 +649,10 @@ class IOBase(object):
try: try:
self._parentdevice._modio._myfh.ioctl( self._parentdevice._modio._myfh.ioctl(
19216, 19216,
self.__bit_ioctl_on if value self.__bit_ioctl_on if value else self.__bit_ioctl_off,
else self.__bit_ioctl_off
) )
except Exception as e: except Exception as e:
self._parentdevice._modio._gotioerror( self._parentdevice._modio._gotioerror("net_ioset", e)
"net_ioset", e)
return False return False
else: else:
@@ -586,8 +661,7 @@ class IOBase(object):
# Set value durchführen (Funktion K+16) # Set value durchführen (Funktion K+16)
self._parentdevice._modio._simulate_ioctl( self._parentdevice._modio._simulate_ioctl(
19216, 19216,
self.__bit_ioctl_on if value self.__bit_ioctl_on if value else self.__bit_ioctl_off,
else self.__bit_ioctl_off
) )
except Exception as e: except Exception as e:
self._parentdevice._modio._gotioerror("file_ioset", e) self._parentdevice._modio._gotioerror("file_ioset", e)
@@ -598,9 +672,7 @@ class IOBase(object):
value = bytes(self._parentdevice._ba_devdata[self._slc_address]) value = bytes(self._parentdevice._ba_devdata[self._slc_address])
with self._parentdevice._modio._myfh_lck: with self._parentdevice._modio._myfh_lck:
try: try:
self._parentdevice._modio._myfh.seek( self._parentdevice._modio._myfh.seek(self._get_address())
self._get_address()
)
self._parentdevice._modio._myfh.write(value) self._parentdevice._modio._myfh.write(value)
if self._parentdevice._modio._buffedwrite: if self._parentdevice._modio._buffedwrite:
self._parentdevice._modio._myfh.flush() self._parentdevice._modio._myfh.flush()
@@ -625,15 +697,11 @@ class IOBase(object):
:return: IO-Wert als <class 'bytes'> oder <class 'bool'> :return: IO-Wert als <class 'bytes'> oder <class 'bool'>
""" """
if self._bitshift: if self._bitshift:
return bool( return bool(self._parentdevice._ba_devdata[self._slc_address.start] & self._bitshift)
self._parentdevice._ba_devdata[self._slc_address.start]
& self._bitshift
)
else: else:
return bytes(self._parentdevice._ba_devdata[self._slc_address]) return bytes(self._parentdevice._ba_devdata[self._slc_address])
def reg_event( def reg_event(self, func, delay=0, edge=BOTH, as_thread=False, prefire=False):
self, func, delay=0, edge=BOTH, as_thread=False, prefire=False):
""" """
Registriert fuer IO ein Event bei der Eventueberwachung. Registriert fuer IO ein Event bei der Eventueberwachung.
@@ -652,8 +720,7 @@ class IOBase(object):
""" """
self.__reg_xevent(func, delay, edge, as_thread, True, prefire) self.__reg_xevent(func, delay, edge, as_thread, True, prefire)
def reg_timerevent( def reg_timerevent(self, func, delay, edge=BOTH, as_thread=False, prefire=False):
self, func, delay, edge=BOTH, as_thread=False, prefire=False):
""" """
Registriert fuer IO einen Timer, welcher nach delay func ausfuehrt. Registriert fuer IO einen Timer, welcher nach delay func ausfuehrt.
@@ -685,20 +752,13 @@ class IOBase(object):
if self._iotype == INP: if self._iotype == INP:
if self._parentdevice._modio._simulator: if self._parentdevice._modio._simulator:
raise RuntimeError( raise RuntimeError(
"can not write to output '{0}' in simulator mode" "can not write to output '{0}' in simulator mode".format(self._name)
"".format(self._name)
) )
else: else:
raise RuntimeError( raise RuntimeError("can not write to input '{0}'".format(self._name))
"can not write to input '{0}'".format(self._name)
)
elif self._iotype == MEM: elif self._iotype == MEM:
raise RuntimeError( raise RuntimeError("can not write to memory '{0}'".format(self._name))
"can not write to memory '{0}'".format(self._name) raise RuntimeError("the io object '{0}' is read only".format(self._name))
)
raise RuntimeError(
"the io object '{0}' is read only".format(self._name)
)
if self._bitshift: if self._bitshift:
# Versuchen egal welchen Typ in Bool zu konvertieren # Versuchen egal welchen Typ in Bool zu konvertieren
@@ -722,8 +782,7 @@ class IOBase(object):
int_byte -= self._bitshift int_byte -= self._bitshift
# Zurückschreiben wenn verändert # Zurückschreiben wenn verändert
self._parentdevice._ba_devdata[self._slc_address.start] = \ self._parentdevice._ba_devdata[self._slc_address.start] = int_byte
int_byte
self._parentdevice._filelock.release() self._parentdevice._filelock.release()
@@ -738,9 +797,7 @@ class IOBase(object):
if self._length != len(value): if self._length != len(value):
raise ValueError( raise ValueError(
"'{0}' requires a <class 'bytes'> object of " "'{0}' requires a <class 'bytes'> object of "
"length {1}, but {2} was given".format( "length {1}, but {2} was given".format(self._name, self._length, len(value))
self._name, self._length, len(value)
)
) )
if self._parentdevice._shared_procimg: if self._parentdevice._shared_procimg:
@@ -764,8 +821,7 @@ class IOBase(object):
else: else:
newlist = [] newlist = []
for regfunc in self._parentdevice._dict_events[self]: for regfunc in self._parentdevice._dict_events[self]:
if regfunc.func != func or edge is not None \ if regfunc.func != func or edge is not None and regfunc.edge != edge:
and regfunc.edge != edge:
newlist.append(regfunc) newlist.append(regfunc)
# Wenn Funktionen übrig bleiben, diese übernehmen # Wenn Funktionen übrig bleiben, diese übernehmen
@@ -832,17 +888,11 @@ class IOBase(object):
"revpimodio2.FALLING or revpimodio2.BOTH" "revpimodio2.FALLING or revpimodio2.BOTH"
) )
if not (exitevent is None or type(exitevent) == Event): if not (exitevent is None or type(exitevent) == Event):
raise TypeError( raise TypeError("parameter 'exitevent' must be <class 'threading.Event'>")
"parameter 'exitevent' must be <class 'threading.Event'>"
)
if type(timeout) != int or timeout < 0: if type(timeout) != int or timeout < 0:
raise ValueError( raise ValueError("parameter 'timeout' must be <class 'int'> and greater than 0")
"parameter 'timeout' must be <class 'int'> and greater than 0"
)
if edge != BOTH and not self._bitshift: if edge != BOTH and not self._bitshift:
raise ValueError( raise ValueError("parameter 'edge' can be used with bit Inputs only")
"parameter 'edge' can be used with bit Inputs only"
)
# Abbruchwert prüfen # Abbruchwert prüfen
if okvalue == self.value: if okvalue == self.value:
@@ -858,23 +908,27 @@ class IOBase(object):
exitevent = Event() exitevent = Event()
flt_timecount = 0 if bool_timecount else -1 flt_timecount = 0 if bool_timecount else -1
while not self._parentdevice._modio._waitexit.is_set() \ while (
and not exitevent.is_set() \ not self._parentdevice._modio._waitexit.is_set()
and flt_timecount < timeout: and not exitevent.is_set()
and flt_timecount < timeout
):
if self._parentdevice._modio._imgwriter.newdata.wait(2.5): if self._parentdevice._modio._imgwriter.newdata.wait(2.5):
self._parentdevice._modio._imgwriter.newdata.clear() self._parentdevice._modio._imgwriter.newdata.clear()
if val_start != self.value: if val_start != self.value:
if edge == BOTH \ if (
or edge == RISING and not val_start \ edge == BOTH
or edge == FALLING and val_start: or edge == RISING
and not val_start
or edge == FALLING
and val_start
):
return 0 return 0
else: else:
val_start = not val_start val_start = not val_start
if bool_timecount: if bool_timecount:
flt_timecount += \ flt_timecount += self._parentdevice._modio._imgwriter._refresh
self._parentdevice._modio._imgwriter._refresh
elif bool_timecount: elif bool_timecount:
flt_timecount += 2.5 flt_timecount += 2.5
@@ -922,7 +976,7 @@ class IntIO(IOBase):
return int.from_bytes( return int.from_bytes(
self._parentdevice._ba_devdata[self._slc_address], self._parentdevice._ba_devdata[self._slc_address],
byteorder=self._byteorder, byteorder=self._byteorder,
signed=self._signed signed=self._signed,
) )
def __call__(self, value=None): def __call__(self, value=None):
@@ -931,16 +985,18 @@ class IntIO(IOBase):
return int.from_bytes( return int.from_bytes(
self._parentdevice._ba_devdata[self._slc_address], self._parentdevice._ba_devdata[self._slc_address],
byteorder=self._byteorder, byteorder=self._byteorder,
signed=self._signed signed=self._signed,
) )
else: else:
# Inline from set_intvalue() # Inline from set_intvalue()
if type(value) == int: if type(value) == int:
self.set_value(value.to_bytes( self.set_value(
value.to_bytes(
self._length, self._length,
byteorder=self._byteorder, byteorder=self._byteorder,
signed=self._signed signed=self._signed,
)) )
)
else: else:
raise TypeError( raise TypeError(
"'{0}' need a <class 'int'> value, but {1} was given" "'{0}' need a <class 'int'> value, but {1} was given"
@@ -983,9 +1039,7 @@ class IntIO(IOBase):
:return: <class 'int'> Defaultvalue :return: <class 'int'> Defaultvalue
""" """
return int.from_bytes( return int.from_bytes(self._defaultvalue, byteorder=self._byteorder, signed=self._signed)
self._defaultvalue, byteorder=self._byteorder, signed=self._signed
)
def get_intvalue(self) -> int: def get_intvalue(self) -> int:
""" """
@@ -996,7 +1050,7 @@ class IntIO(IOBase):
return int.from_bytes( return int.from_bytes(
self._parentdevice._ba_devdata[self._slc_address], self._parentdevice._ba_devdata[self._slc_address],
byteorder=self._byteorder, byteorder=self._byteorder,
signed=self._signed signed=self._signed,
) )
def set_intvalue(self, value: int) -> None: def set_intvalue(self, value: int) -> None:
@@ -1006,11 +1060,13 @@ class IntIO(IOBase):
:param value: <class 'int'> Wert :param value: <class 'int'> Wert
""" """
if type(value) == int: if type(value) == int:
self.set_value(value.to_bytes( self.set_value(
value.to_bytes(
self._length, self._length,
byteorder=self._byteorder, byteorder=self._byteorder,
signed=self._signed signed=self._signed,
)) )
)
else: else:
raise TypeError( raise TypeError(
"'{0}' need a <class 'int'> value, but {1} was given" "'{0}' need a <class 'int'> value, but {1} was given"
@@ -1028,9 +1084,7 @@ class IntIOCounter(IntIO):
__slots__ = ("__ioctl_arg",) __slots__ = ("__ioctl_arg",)
def __init__( def __init__(self, counter_id, parentdevice, valuelist, iotype, byteorder, signed):
self, counter_id,
parentdevice, valuelist, iotype, byteorder, signed):
""" """
Instantiierung der IntIOCounter-Klasse. Instantiierung der IntIOCounter-Klasse.
@@ -1044,10 +1098,11 @@ class IntIOCounter(IntIO):
# Deviceposition + leer + Counter_ID # Deviceposition + leer + Counter_ID
# ID-Bits: 7|6|5|4|3|2|1|0|15|14|13|12|11|10|9|8 # ID-Bits: 7|6|5|4|3|2|1|0|15|14|13|12|11|10|9|8
self.__ioctl_arg = \ self.__ioctl_arg = (
parentdevice._position.to_bytes(1, "little") \ parentdevice._position.to_bytes(1, "little")
+ b'\x00' \ + b"\x00"
+ (1 << counter_id).to_bytes(2, "little") + (1 << counter_id).to_bytes(2, "little")
)
""" """
IOCTL fuellt dieses struct, welches durch padding im Speicher nach IOCTL fuellt dieses struct, welches durch padding im Speicher nach
@@ -1067,23 +1122,16 @@ class IntIOCounter(IntIO):
def reset(self) -> None: def reset(self) -> None:
"""Setzt den Counter des Inputs zurueck.""" """Setzt den Counter des Inputs zurueck."""
if self._parentdevice._modio._monitoring: if self._parentdevice._modio._monitoring:
raise RuntimeError( raise RuntimeError("can not reset counter, while system is in monitoring mode")
"can not reset counter, while system is in monitoring mode"
)
if self._parentdevice._modio._simulator: if self._parentdevice._modio._simulator:
raise RuntimeError( raise RuntimeError("can not reset counter, while system is in simulator mode")
"can not reset counter, while system is in simulator mode"
)
if self._parentdevice._modio._run_on_pi: if self._parentdevice._modio._run_on_pi:
# IOCTL auf dem RevPi # IOCTL auf dem RevPi
with self._parentdevice._modio._myfh_lck: with self._parentdevice._modio._myfh_lck:
try: try:
# Counter reset durchführen (Funktion K+20) # Counter reset durchführen (Funktion K+20)
ioctl( ioctl(self._parentdevice._modio._myfh, 19220, self.__ioctl_arg)
self._parentdevice._modio._myfh,
19220, self.__ioctl_arg
)
except Exception as e: except Exception as e:
self._parentdevice._modio._gotioerror("iorst", e) self._parentdevice._modio._gotioerror("iorst", e)
@@ -1091,9 +1139,7 @@ class IntIOCounter(IntIO):
# IOCTL über Netzwerk # IOCTL über Netzwerk
with self._parentdevice._modio._myfh_lck: with self._parentdevice._modio._myfh_lck:
try: try:
self._parentdevice._modio._myfh.ioctl( self._parentdevice._modio._myfh.ioctl(19220, self.__ioctl_arg)
19220, self.__ioctl_arg
)
except Exception as e: except Exception as e:
self._parentdevice._modio._gotioerror("net_iorst", e) self._parentdevice._modio._gotioerror("net_iorst", e)
@@ -1101,9 +1147,7 @@ class IntIOCounter(IntIO):
# IOCTL in Datei simulieren # IOCTL in Datei simulieren
try: try:
# Set value durchführen (Funktion K+20) # Set value durchführen (Funktion K+20)
self._parentdevice._modio._simulate_ioctl( self._parentdevice._modio._simulate_ioctl(19220, self.__ioctl_arg)
19220, self.__ioctl_arg
)
except Exception as e: except Exception as e:
self._parentdevice._modio._gotioerror("file_iorst", e) self._parentdevice._modio._gotioerror("file_iorst", e)
@@ -1153,12 +1197,7 @@ class IntIOReplaceable(IntIO):
`<https://docs.python.org/3/library/struct.html#format-characters>`_ `<https://docs.python.org/3/library/struct.html#format-characters>`_
""" """
# StructIO erzeugen # StructIO erzeugen
io_new = StructIO( io_new = StructIO(self, name, frm, **kwargs)
self,
name,
frm,
**kwargs
)
# StructIO in IO-Liste einfügen # StructIO in IO-Liste einfügen
self._parentdevice._modio.io._private_register_new_io_object(io_new) self._parentdevice._modio.io._private_register_new_io_object(io_new)
@@ -1170,10 +1209,129 @@ class IntIOReplaceable(IntIO):
reg_event, reg_event,
kwargs.get("delay", 0), kwargs.get("delay", 0),
kwargs.get("edge", BOTH), kwargs.get("edge", BOTH),
kwargs.get("as_thread", False) kwargs.get("as_thread", False),
) )
class RelaisOutput(IOBase):
"""
Class for relais outputs to access the cycle counters.
This class extends the function of <class 'IOBase'> to the function
'get_cycles' and the property 'cycles' to retrieve the relay cycle
counters.
:ref: :class:`IOBase`
"""
def __init__(self, parentdevice, valuelist, iotype, byteorder, signed):
"""
Extend <class 'IOBase'> with functions to access cycle counters.
:ref: :func:`IOBase.__init__(...)`
"""
super().__init__(parentdevice, valuelist, iotype, byteorder, signed)
"""
typedef struct SROGetCountersStr
{
/* Address of module in current configuration */
uint8_t i8uAddress;
uint32_t counter[REVPI_RO_NUM_RELAY_COUNTERS];
} SROGetCounters;
"""
# Device position + padding + four counter with 4 byte each
self.__ioctl_arg_format = "<BIIII"
self.__ioctl_arg = struct.pack(
self.__ioctl_arg_format,
parentdevice._position,
0,
0,
0,
0,
)
def get_switching_cycles(self):
"""
Get the number of switching cycles from this relay.
If each relay output is represented as BOOL, this function returns a
single integer value. If all relays are displayed as a BYTE, this
function returns a tuple that contains the values of all relay outputs.
The setting is determined by PiCtory and the selected output variant by
the RO device.
This function is only available locally on a Revolution Pi. This
function cannot be used via RevPiNetIO.
:return: Integer of switching cycles as single value or tuple of all
"""
# Using ioctl request K+29 = 19229
if self._parentdevice._modio._run_on_pi:
# IOCTL to piControl on the RevPi
with self._parentdevice._modio._myfh_lck:
try:
ioctl_return_value = ioctl(
self._parentdevice._modio._myfh,
19229,
self.__ioctl_arg,
)
except Exception as e:
# If not implemented, we return the max value and set an error
ioctl_return_value = b"\xff" * struct.calcsize(self.__ioctl_arg_format)
self._parentdevice._modio._gotioerror("rocounter", e)
elif hasattr(self._parentdevice._modio._myfh, "ioctl"):
# IOCTL over network
"""
The ioctl function over the network does not return a value. Only the successful
execution of the ioctl call is checked and reported back. If a new function has been
implemented in RevPiPyLoad, the subsequent source code can be activated.
with self._parentdevice._modio._myfh_lck:
try:
ioctl_return_value = self._parentdevice._modio._myfh.ioctl(
19229, self.__ioctl_arg
)
except Exception as e:
self._parentdevice._modio._gotioerror("net_rocounter", e)
"""
raise RuntimeError("Can not be called over network via RevPiNetIO")
else:
# Simulate IOCTL on a regular file returns the value of relais index
ioctl_return_value = self.__ioctl_arg
if self._bitaddress == -1:
# Return cycle values of all relais as tuple, if this is a BYTE output
# Remove fist element, which is the ioctl request value
return struct.unpack(self.__ioctl_arg_format, ioctl_return_value)[1:]
else:
# Return cycle value of just one relais as int, if this is a BOOL output
# Increase bit-address bei 1 to ignore fist element, which is the ioctl request value
return struct.unpack(self.__ioctl_arg_format, ioctl_return_value)[self._bitaddress + 1]
switching_cycles = property(get_switching_cycles)
class IntRelaisOutput(IntIO, RelaisOutput):
"""
Class for relais outputs to access the cycle counters.
This class combines the function of <class 'IntIO'> and
<class 'RelaisOutput'> to add the function 'get_cycles' and the property
'cycles' to retrieve the relay cycle counters.
Since both classes inherit from BaseIO, both __init__ functions are called
and the logic is combined. In this case, there is only one 'self' object of
IOBase, which of both classes in inheritance is extended with this.
:ref: :class:`IOBase`
"""
pass
class StructIO(IOBase): class StructIO(IOBase):
""" """
Klasse fuer den Zugriff auf Daten ueber ein definierten struct. Klasse fuer den Zugriff auf Daten ueber ein definierten struct.
@@ -1182,8 +1340,14 @@ class StructIO(IOBase):
bereit. Der struct-Formatwert wird bei der Instantiierung festgelegt. bereit. Der struct-Formatwert wird bei der Instantiierung festgelegt.
""" """
__slots__ = "__frm", "_parentio_address", "_parentio_defaultvalue", \ __slots__ = (
"_parentio_length", "_parentio_name", "_wordorder" "__frm",
"_parentio_address",
"_parentio_defaultvalue",
"_parentio_length",
"_parentio_name",
"_wordorder",
)
def __init__(self, parentio, name: str, frm: str, **kwargs): def __init__(self, parentio, name: str, frm: str, **kwargs):
""" """
@@ -1215,15 +1379,12 @@ class StructIO(IOBase):
if frm == "?": if frm == "?":
if self._wordorder: if self._wordorder:
raise ValueError( raise ValueError("you can not use wordorder for bit based ios")
"you can not use wordorder for bit based ios"
)
bitaddress = kwargs.get("bit", 0) bitaddress = kwargs.get("bit", 0)
max_bits = parentio._length * 8 max_bits = parentio._length * 8
if not (0 <= bitaddress < max_bits): if not (0 <= bitaddress < max_bits):
raise ValueError( raise ValueError(
"bitaddress must be a value between 0 and {0}" "bitaddress must be a value between 0 and {0}".format(max_bits - 1)
"".format(max_bits - 1)
) )
bitlength = 1 bitlength = 1
@@ -1246,8 +1407,7 @@ class StructIO(IOBase):
raise ValueError("wordorder must be 'little' or 'big'") raise ValueError("wordorder must be 'little' or 'big'")
if byte_length % 2 != 0: if byte_length % 2 != 0:
raise ValueError( raise ValueError(
"the byte length of new io must must be even to " "the byte length of new io must must be even to use wordorder"
"use wordorder"
) )
# [name,default,anzbits,adressbyte,export,adressid,bmk,bitaddress] # [name,default,anzbits,adressbyte,export,adressid,bmk,bitaddress]
@@ -1260,7 +1420,7 @@ class StructIO(IOBase):
False, False,
str(parentio._slc_address.start).rjust(4, "0"), str(parentio._slc_address.start).rjust(4, "0"),
kwargs.get("bmk", ""), kwargs.get("bmk", ""),
bitaddress bitaddress,
] ]
else: else:
raise ValueError( raise ValueError(
@@ -1270,11 +1430,7 @@ class StructIO(IOBase):
# Basisklasse instantiieren # Basisklasse instantiieren
super().__init__( super().__init__(
parentio._parentdevice, parentio._parentdevice, valuelist, parentio._iotype, byteorder, frm == frm.lower()
valuelist,
parentio._iotype,
byteorder,
frm == frm.lower()
) )
self.__frm = bofrm + frm self.__frm = bofrm + frm
if "export" in kwargs: if "export" in kwargs:
@@ -1286,13 +1442,11 @@ class StructIO(IOBase):
self._export = parentio._export self._export = parentio._export
# Platz für neuen IO prüfen # Platz für neuen IO prüfen
if not (self._slc_address.start >= if not (
parentio._parentdevice._dict_slc[parentio._iotype].start and self._slc_address.start >= parentio._parentdevice._dict_slc[parentio._iotype].start
self._slc_address.stop <= and self._slc_address.stop <= parentio._parentdevice._dict_slc[parentio._iotype].stop
parentio._parentdevice._dict_slc[parentio._iotype].stop): ):
raise BufferError( raise BufferError("registered value does not fit process image scope")
"registered value does not fit process image scope"
)
def __call__(self, value=None): def __call__(self, value=None):
if value is None: if value is None:
@@ -1310,9 +1464,7 @@ class StructIO(IOBase):
if self._bitshift: if self._bitshift:
self.set_value(value) self.set_value(value)
elif self._wordorder == "little" and self._length > 2: elif self._wordorder == "little" and self._length > 2:
self.set_value( self.set_value(self._swap_word_order(struct.pack(self.__frm, value)))
self._swap_word_order(struct.pack(self.__frm, value))
)
else: else:
self.set_value(struct.pack(self.__frm, value)) self.set_value(struct.pack(self.__frm, value))
@@ -1343,8 +1495,10 @@ class StructIO(IOBase):
array_length = len(bytes_to_swap) array_length = len(bytes_to_swap)
swap_array = bytearray(bytes_to_swap) swap_array = bytearray(bytes_to_swap)
for i in range(0, array_length // 2, 2): for i in range(0, array_length // 2, 2):
swap_array[-i - 2:array_length - i], swap_array[i:i + 2] = \ swap_array[-i - 2 : array_length - i], swap_array[i : i + 2] = (
swap_array[i:i + 2], swap_array[-i - 2:array_length - i] swap_array[i : i + 2],
swap_array[-i - 2 : array_length - i],
)
return bytes(swap_array) return bytes(swap_array)
def get_structdefaultvalue(self): def get_structdefaultvalue(self):
@@ -1394,9 +1548,7 @@ class StructIO(IOBase):
if self._bitshift: if self._bitshift:
self.set_value(value) self.set_value(value)
elif self._wordorder == "little" and self._length > 2: elif self._wordorder == "little" and self._length > 2:
self.set_value( self.set_value(self._swap_word_order(struct.pack(self.__frm, value)))
self._swap_word_order(struct.pack(self.__frm, value))
)
else: else:
self.set_value(struct.pack(self.__frm, value)) self.set_value(struct.pack(self.__frm, value))
@@ -1422,7 +1574,7 @@ class MemIO(IOBase):
if self._bitlength > 64: if self._bitlength > 64:
# STRING # STRING
try: try:
val = val.strip(b'\x00').decode() val = val.strip(b"\x00").decode()
except Exception: except Exception:
pass pass
return val return val

View File

@@ -65,20 +65,55 @@ class RevPiModIO(object):
Device Positionen oder Device Namen. Device Positionen oder Device Namen.
""" """
__slots__ = "__cleanupfunc", \ __slots__ = (
"_autorefresh", "_buffedwrite", "_configrsc", "_debug", "_devselect", \ "__cleanupfunc",
"_exit", "_exit_level", "_imgwriter", "_ioerror", \ "_autorefresh",
"_length", "_looprunning", "_lst_devselect", "_lst_refresh", \ "_buffedwrite",
"_maxioerrors", "_monitoring", "_myfh", "_myfh_lck", \ "_configrsc",
"_procimg", "_replace_io_file", "_run_on_pi", \ "_context_manager",
"_set_device_based_cycle_time", "_simulator", "_init_shared_procimg", \ "_debug",
"_syncoutputs", "_th_mainloop", "_waitexit", \ "_devselect",
"app", "core", "device", "exitsignal", "io", "summary" "_exit",
"_exit_level",
"_imgwriter",
"_ioerror",
"_length",
"_looprunning",
"_lst_devselect",
"_lst_refresh",
"_maxioerrors",
"_monitoring",
"_myfh",
"_myfh_lck",
"_procimg",
"_replace_io_file",
"_run_on_pi",
"_set_device_based_cycle_time",
"_simulator",
"_init_shared_procimg",
"_syncoutputs",
"_th_mainloop",
"_waitexit",
"app",
"core",
"device",
"exitsignal",
"io",
"summary",
)
def __init__( def __init__(
self, autorefresh=False, monitoring=False, syncoutputs=True, self,
procimg=None, configrsc=None, simulator=False, debug=True, autorefresh=False,
replace_io_file=None, shared_procimg=False, direct_output=False): monitoring=False,
syncoutputs=True,
procimg=None,
configrsc=None,
simulator=False,
debug=True,
replace_io_file=None,
shared_procimg=False,
):
""" """
Instantiiert die Grundfunktionen. Instantiiert die Grundfunktionen.
@@ -92,32 +127,32 @@ class RevPiModIO(object):
:param replace_io_file: Replace IO Konfiguration aus Datei laden :param replace_io_file: Replace IO Konfiguration aus Datei laden
:param shared_procimg: Share process image with other processes, this :param shared_procimg: Share process image with other processes, this
could be insecure for automation could be insecure for automation
:param direct_output: Deprecated, use shared_procimg
""" """
# Parameterprüfung # Parameterprüfung
acheck( acheck(
bool, autorefresh=autorefresh, monitoring=monitoring, bool,
syncoutputs=syncoutputs, simulator=simulator, debug=debug, autorefresh=autorefresh,
shared_procimg=shared_procimg, direct_output=direct_output monitoring=monitoring,
syncoutputs=syncoutputs,
simulator=simulator,
debug=debug,
shared_procimg=shared_procimg,
) )
acheck( acheck(
str, procimg_noneok=procimg, configrsc_noneok=configrsc, str,
replace_io_file_noneok=replace_io_file procimg_noneok=procimg,
configrsc_noneok=configrsc,
replace_io_file_noneok=replace_io_file,
) )
# TODO: Remove in next release
if direct_output:
warnings.warn(DeprecationWarning(
"direct_output is deprecated - use shared_procimg instead!"
))
self._autorefresh = autorefresh self._autorefresh = autorefresh
self._configrsc = configrsc self._configrsc = configrsc
self._context_manager = False
self._monitoring = monitoring self._monitoring = monitoring
self._procimg = "/dev/piControl0" if procimg is None else procimg self._procimg = "/dev/piControl0" if procimg is None else procimg
self._set_device_based_cycle_time = True self._set_device_based_cycle_time = True
self._simulator = simulator self._simulator = simulator
self._init_shared_procimg = shared_procimg or direct_output self._init_shared_procimg = shared_procimg
self._syncoutputs = syncoutputs self._syncoutputs = syncoutputs
# TODO: bei simulator und procimg prüfen ob datei existiert / anlegen? # TODO: bei simulator und procimg prüfen ob datei existiert / anlegen?
@@ -172,6 +207,22 @@ class RevPiModIO(object):
if self._myfh is not None: if self._myfh is not None:
self._myfh.close() self._myfh.close()
def __enter__(self):
if self._context_manager:
raise RuntimeError("can not use multiple context managers of same instance")
if self._looprunning:
raise RuntimeError("can not enter context manager with running mainloop or cycleloop")
self._context_manager = True
self._looprunning = True
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.writeprocimg()
self.exit(full=True)
self._looprunning = False
self._context_manager = False
def __evt_exit(self, signum, sigframe) -> None: def __evt_exit(self, signum, sigframe) -> None:
""" """
Eventhandler fuer Programmende. Eventhandler fuer Programmende.
@@ -233,13 +284,11 @@ class RevPiModIO(object):
# Apply device filter # Apply device filter
if self._devselect.values: if self._devselect.values:
# Check for supported types in values # Check for supported types in values
for dev in self._devselect.values: for dev in self._devselect.values:
if type(dev) not in (int, str): if type(dev) not in (int, str):
raise ValueError( raise ValueError(
"need device position as <class 'int'> or " "need device position as <class 'int'> or device name as <class 'str'>"
"device name as <class 'str'>"
) )
lst_devices = [] lst_devices = []
@@ -253,8 +302,10 @@ class RevPiModIO(object):
continue continue
else: else:
# Auto search depending of value item type # Auto search depending of value item type
if not (dev["name"] in self._devselect.values if not (
or int(dev["position"]) in self._devselect.values): dev["name"] in self._devselect.values
or int(dev["position"]) in self._devselect.values
):
continue continue
lst_devices.append(dev) lst_devices.append(dev)
@@ -264,11 +315,23 @@ class RevPiModIO(object):
# Device und IO Klassen anlegen # Device und IO Klassen anlegen
self.device = devicemodule.DeviceList() self.device = devicemodule.DeviceList()
self.io = IOList() self.io = IOList(self)
# Devices initialisieren # Devices initialisieren
err_names_check = {} err_names_check = {}
for device in sorted(lst_devices, key=lambda x: x["offset"]): for device in sorted(lst_devices, key=lambda x: x["offset"]):
# Pre-check of values
if float(device.get("offset")) != int(device.get("offset")):
# Offset misconfigured
warnings.warn(
"Offset value {0} of device {1} on position {2} is invalid. "
"This device and all IOs are ignored.".format(
device.get("offset"),
device.get("name"),
device.get("position"),
)
)
continue
# VDev alter piCtory Versionen auf KUNBUS-Standard ändern # VDev alter piCtory Versionen auf KUNBUS-Standard ändern
if device["position"] == "adap.": if device["position"] == "adap.":
@@ -281,64 +344,45 @@ class RevPiModIO(object):
pt = int(device["productType"]) pt = int(device["productType"])
if pt == ProductType.REVPI_CORE: if pt == ProductType.REVPI_CORE:
# RevPi Core # RevPi Core
dev_new = devicemodule.Core( dev_new = devicemodule.Core(self, device, simulator=self._simulator)
self, device, simulator=self._simulator
)
self.core = dev_new self.core = dev_new
elif pt == ProductType.REVPI_CONNECT: elif pt == ProductType.REVPI_CONNECT:
# RevPi Connect # RevPi Connect
dev_new = devicemodule.Connect( dev_new = devicemodule.Connect(self, device, simulator=self._simulator)
self, device, simulator=self._simulator
)
self.core = dev_new self.core = dev_new
elif pt == ProductType.REVPI_CONNECT_4: elif pt == ProductType.REVPI_CONNECT_4:
# RevPi Connect 4 # RevPi Connect 4
dev_new = devicemodule.Connect4( dev_new = devicemodule.Connect4(self, device, simulator=self._simulator)
self, device, simulator=self._simulator
)
self.core = dev_new self.core = dev_new
elif pt == ProductType.REVPI_COMPACT: elif pt == ProductType.REVPI_COMPACT:
# RevPi Compact # RevPi Compact
dev_new = devicemodule.Compact( dev_new = devicemodule.Compact(self, device, simulator=self._simulator)
self, device, simulator=self._simulator
)
self.core = dev_new self.core = dev_new
elif pt == ProductType.REVPI_FLAT: elif pt == ProductType.REVPI_FLAT:
# RevPi Flat # RevPi Flat
dev_new = devicemodule.Flat( dev_new = devicemodule.Flat(self, device, simulator=self._simulator)
self, device, simulator=self._simulator
)
self.core = dev_new self.core = dev_new
else: else:
# Base immer als Fallback verwenden # Base immer als Fallback verwenden
dev_new = devicemodule.Base( dev_new = devicemodule.Base(self, device, simulator=self._simulator)
self, device, simulator=self._simulator
)
elif device["type"] == DeviceType.LEFT_RIGHT: elif device["type"] == DeviceType.LEFT_RIGHT:
# IOs # IOs
pt = int(device["productType"]) pt = int(device["productType"])
if pt == ProductType.DIO \ if pt == ProductType.DIO or pt == ProductType.DI or pt == ProductType.DO:
or pt == ProductType.DI \
or pt == ProductType.DO:
# DIO / DI / DO # DIO / DI / DO
dev_new = devicemodule.DioModule( dev_new = devicemodule.DioModule(self, device, simulator=self._simulator)
self, device, simulator=self._simulator elif pt == ProductType.RO:
) # RO
dev_new = devicemodule.RoModule(self, device, simulator=self._simulator)
else: else:
# Alle anderen IO-Devices # Alle anderen IO-Devices
dev_new = devicemodule.Device( dev_new = devicemodule.Device(self, device, simulator=self._simulator)
self, device, simulator=self._simulator
)
elif device["type"] == DeviceType.VIRTUAL: elif device["type"] == DeviceType.VIRTUAL:
# Virtuals # Virtuals
dev_new = devicemodule.Virtual( dev_new = devicemodule.Virtual(self, device, simulator=self._simulator)
self, device, simulator=self._simulator
)
elif device["type"] == DeviceType.EDGE: elif device["type"] == DeviceType.EDGE:
# Gateways # Gateways
dev_new = devicemodule.Gateway( dev_new = devicemodule.Gateway(self, device, simulator=self._simulator)
self, device, simulator=self._simulator
)
elif device["type"] == DeviceType.RIGHT: elif device["type"] == DeviceType.RIGHT:
# Connectdevice # Connectdevice
dev_new = None dev_new = None
@@ -347,7 +391,7 @@ class RevPiModIO(object):
warnings.warn( warnings.warn(
"device type '{0}' on position {1} unknown" "device type '{0}' on position {1} unknown"
"".format(device["type"], device["position"]), "".format(device["type"], device["position"]),
Warning Warning,
) )
dev_new = None dev_new = None
@@ -378,7 +422,7 @@ class RevPiModIO(object):
"equal device name '{0}' in pictory configuration. you can " "equal device name '{0}' in pictory configuration. you can "
"access this devices by position number .device[{1}] only!" "access this devices by position number .device[{1}] only!"
"".format(check_dev, "|".join(err_names_check[check_dev])), "".format(check_dev, "|".join(err_names_check[check_dev])),
Warning Warning,
) )
# ImgWriter erstellen # ImgWriter erstellen
@@ -393,17 +437,12 @@ class RevPiModIO(object):
self.syncoutputs() self.syncoutputs()
# Für RS485 errors am core defaults laden sollte procimg NULL sein # Für RS485 errors am core defaults laden sollte procimg NULL sein
if isinstance(self.core, devicemodule.Core) and \ if isinstance(self.core, devicemodule.Core) and not (self._monitoring or self._simulator):
not (self._monitoring or self._simulator):
if self.core._slc_errorlimit1 is not None: if self.core._slc_errorlimit1 is not None:
io = self.io[ io = self.io[self.core.offset + self.core._slc_errorlimit1.start][0]
self.core.offset + self.core._slc_errorlimit1.start
][0]
io.set_value(io._defaultvalue) io.set_value(io._defaultvalue)
if self.core._slc_errorlimit2 is not None: if self.core._slc_errorlimit2 is not None:
io = self.io[ io = self.io[self.core.offset + self.core._slc_errorlimit2.start][0]
self.core.offset + self.core._slc_errorlimit2.start
][0]
io.set_value(io._defaultvalue) io.set_value(io._defaultvalue)
# RS485 errors schreiben # RS485 errors schreiben
@@ -471,8 +510,7 @@ class RevPiModIO(object):
if "defaultvalue" in creplaceio[io]: if "defaultvalue" in creplaceio[io]:
if dict_replace["frm"] == "?": if dict_replace["frm"] == "?":
try: try:
dict_replace["defaultvalue"] = \ dict_replace["defaultvalue"] = creplaceio[io].getboolean("defaultvalue")
creplaceio[io].getboolean("defaultvalue")
except Exception: except Exception:
raise ValueError( raise ValueError(
"replace_io_file: could not convert '{0}' " "replace_io_file: could not convert '{0}' "
@@ -494,8 +532,7 @@ class RevPiModIO(object):
) )
else: else:
try: try:
dict_replace["defaultvalue"] = \ dict_replace["defaultvalue"] = creplaceio[io].getint("defaultvalue")
creplaceio[io].getint("defaultvalue")
except Exception: except Exception:
raise ValueError( raise ValueError(
"replace_io_file: could not convert '{0}' " "replace_io_file: could not convert '{0}' "
@@ -634,23 +671,19 @@ class RevPiModIO(object):
self._ioerror += 1 self._ioerror += 1
if self._maxioerrors != 0 and self._ioerror >= self._maxioerrors: if self._maxioerrors != 0 and self._ioerror >= self._maxioerrors:
raise RuntimeError( raise RuntimeError(
"reach max io error count {0} on process image" "reach max io error count {0} on process image".format(self._maxioerrors)
"".format(self._maxioerrors)
) )
if not show_warn or self._debug == -1: if not show_warn or self._debug == -1:
return return
if self._debug == 0: if self._debug == 0:
warnings.warn( warnings.warn("got io error on process image", RuntimeWarning)
"got io error on process image",
RuntimeWarning
)
else: else:
warnings.warn( warnings.warn(
"got io error during '{0}' and count {1} errors now | {2}" "got io error during '{0}' and count {1} errors now | {2}"
"".format(action, self._ioerror, str(e)), "".format(action, self._ioerror, str(e)),
RuntimeWarning RuntimeWarning,
) )
def _set_cycletime(self, milliseconds: int) -> None: def _set_cycletime(self, milliseconds: int) -> None:
@@ -660,10 +693,7 @@ class RevPiModIO(object):
:param milliseconds: <class 'int'> in Millisekunden :param milliseconds: <class 'int'> in Millisekunden
""" """
if self._looprunning: if self._looprunning:
raise RuntimeError( raise RuntimeError("can not change cycletime when cycleloop or mainloop is running")
"can not change cycletime when cycleloop or mainloop is "
"running"
)
else: else:
self._imgwriter.refresh = milliseconds self._imgwriter.refresh = milliseconds
@@ -701,7 +731,7 @@ class RevPiModIO(object):
else: else:
raise ValueError("value must be 0 or a positive integer") raise ValueError("value must be 0 or a positive integer")
def _simulate_ioctl(self, request: int, arg=b'') -> None: def _simulate_ioctl(self, request: int, arg=b"") -> None:
""" """
Simuliert IOCTL Funktionen auf procimg Datei. Simuliert IOCTL Funktionen auf procimg Datei.
@@ -717,9 +747,7 @@ class RevPiModIO(object):
# Simulatonsmodus schreibt direkt in Datei # Simulatonsmodus schreibt direkt in Datei
with self._myfh_lck: with self._myfh_lck:
self._myfh.seek(byte_address) self._myfh.seek(byte_address)
int_byte = int.from_bytes( int_byte = int.from_bytes(self._myfh.read(1), byteorder="little")
self._myfh.read(1), byteorder="little"
)
int_bit = 1 << bit_address int_bit = 1 << bit_address
if not bool(int_byte & int_bit) == new_value: if not bool(int_byte & int_bit) == new_value:
@@ -741,8 +769,9 @@ class RevPiModIO(object):
for i in range(16): for i in range(16):
if bool(bit_field & 1 << i): if bool(bit_field & 1 << i):
io_byte = self.device[dev_position].offset \ io_byte = self.device[dev_position].offset + int(
+ int(self.device[dev_position]._lst_counter[i]) self.device[dev_position]._lst_counter[i]
)
break break
if io_byte == -1: if io_byte == -1:
@@ -750,7 +779,7 @@ class RevPiModIO(object):
with self._myfh_lck: with self._myfh_lck:
self._myfh.seek(io_byte) self._myfh.seek(io_byte)
self._myfh.write(b'\x00\x00\x00\x00') self._myfh.write(b"\x00\x00\x00\x00")
if self._buffedwrite: if self._buffedwrite:
self._myfh.flush() self._myfh.flush()
@@ -794,11 +823,12 @@ class RevPiModIO(object):
:param blocking: Wenn False, blockiert das Programm hier NICHT :param blocking: Wenn False, blockiert das Programm hier NICHT
:return: None or the return value of the cycle function :return: None or the return value of the cycle function
""" """
# Check for context manager
if self._context_manager:
raise RuntimeError("Can not start cycleloop inside a context manager (with statement)")
# Prüfen ob ein Loop bereits läuft # Prüfen ob ein Loop bereits läuft
if self._looprunning: if self._looprunning:
raise RuntimeError( raise RuntimeError("can not start multiple loops mainloop/cycleloop")
"can not start multiple loops mainloop/cycleloop"
)
# Prüfen ob Devices in autorefresh sind # Prüfen ob Devices in autorefresh sind
if len(self._lst_refresh) == 0: if len(self._lst_refresh) == 0:
@@ -809,16 +839,14 @@ class RevPiModIO(object):
# Prüfen ob Funktion callable ist # Prüfen ob Funktion callable ist
if not callable(func): if not callable(func):
raise RuntimeError( raise RuntimeError("registered function '{0}' ist not callable".format(func))
"registered function '{0}' ist not callable".format(func)
)
# Thread erstellen, wenn nicht blockieren soll # Thread erstellen, wenn nicht blockieren soll
if not blocking: if not blocking:
self._th_mainloop = Thread( self._th_mainloop = Thread(
target=self.cycleloop, target=self.cycleloop,
args=(func,), args=(func,),
kwargs={"cycletime": cycletime, "blocking": True} kwargs={"cycletime": cycletime, "blocking": True},
) )
self._th_mainloop.start() self._th_mainloop.start()
return return
@@ -851,9 +879,9 @@ class RevPiModIO(object):
break break
# Just warn, user has to use maxioerrors to kill program # Just warn, user has to use maxioerrors to kill program
warnings.warn(RuntimeWarning( warnings.warn(
"no new io data in cycle loop for 2500 milliseconds" RuntimeWarning("no new io data in cycle loop for 2500 milliseconds")
)) )
cycleinfo.last = self._exit.is_set() cycleinfo.last = self._exit.is_set()
continue continue
@@ -942,7 +970,6 @@ class RevPiModIO(object):
cp = ConfigParser() cp = ConfigParser()
for io in self.io: for io in self.io:
if isinstance(io, StructIO): if isinstance(io, StructIO):
# Required values # Required values
cp.add_section(io.name) cp.add_section(io.name)
cp[io.name]["replace"] = io._parentio_name cp[io.name]["replace"] = io._parentio_name
@@ -958,8 +985,7 @@ class RevPiModIO(object):
if type(io.defaultvalue) is bytes: if type(io.defaultvalue) is bytes:
if any(io.defaultvalue): if any(io.defaultvalue):
# Convert each byte to an integer # Convert each byte to an integer
cp[io.name]["defaultvalue"] = \ cp[io.name]["defaultvalue"] = " ".join(map(str, io.defaultvalue))
" ".join(map(str, io.defaultvalue))
elif io.defaultvalue != 0: elif io.defaultvalue != 0:
cp[io.name]["defaultvalue"] = str(io.defaultvalue) cp[io.name]["defaultvalue"] = str(io.defaultvalue)
if io.bmk != "": if io.bmk != "":
@@ -971,10 +997,7 @@ class RevPiModIO(object):
with open(filename, "w") as fh: with open(filename, "w") as fh:
cp.write(fh) cp.write(fh)
except Exception as e: except Exception as e:
raise RuntimeError( raise RuntimeError("could not write export file '{0}' | {1}".format(filename, e))
"could not write export file '{0}' | {1}"
"".format(filename, e)
)
def get_jconfigrsc(self) -> dict: def get_jconfigrsc(self) -> dict:
""" """
@@ -986,8 +1009,8 @@ class RevPiModIO(object):
if self._configrsc is not None: if self._configrsc is not None:
if not access(self._configrsc, F_OK | R_OK): if not access(self._configrsc, F_OK | R_OK):
raise RuntimeError( raise RuntimeError(
"can not access pictory configuration at {0}".format( "can not access pictory configuration at {0}".format(self._configrsc)
self._configrsc)) )
else: else:
# piCtory Konfiguration an bekannten Stellen prüfen # piCtory Konfiguration an bekannten Stellen prüfen
lst_rsc = ["/etc/revpi/config.rsc", "/opt/KUNBUS/config.rsc"] lst_rsc = ["/etc/revpi/config.rsc", "/opt/KUNBUS/config.rsc"]
@@ -1035,10 +1058,7 @@ class RevPiModIO(object):
""" """
# Prüfen ob Funktion callable ist # Prüfen ob Funktion callable ist
if not (cleanupfunc is None or callable(cleanupfunc)): if not (cleanupfunc is None or callable(cleanupfunc)):
raise RuntimeError( raise RuntimeError("registered function '{0}' ist not callable".format(cleanupfunc))
"registered function '{0}' ist not callable"
"".format(cleanupfunc)
)
self.__cleanupfunc = cleanupfunc self.__cleanupfunc = cleanupfunc
signal(SIGINT, self.__evt_exit) signal(SIGINT, self.__evt_exit)
signal(SIGTERM, self.__evt_exit) signal(SIGTERM, self.__evt_exit)
@@ -1061,11 +1081,12 @@ class RevPiModIO(object):
:param blocking: Wenn False, blockiert das Programm hier NICHT :param blocking: Wenn False, blockiert das Programm hier NICHT
""" """
# Check for context manager
if self._context_manager:
raise RuntimeError("Can not start mainloop inside a context manager (with statement)")
# Prüfen ob ein Loop bereits läuft # Prüfen ob ein Loop bereits läuft
if self._looprunning: if self._looprunning:
raise RuntimeError( raise RuntimeError("can not start multiple loops mainloop/cycleloop")
"can not start multiple loops mainloop/cycleloop"
)
# Prüfen ob Devices in autorefresh sind # Prüfen ob Devices in autorefresh sind
if len(self._lst_refresh) == 0: if len(self._lst_refresh) == 0:
@@ -1076,9 +1097,7 @@ class RevPiModIO(object):
# Thread erstellen, wenn nicht blockieren soll # Thread erstellen, wenn nicht blockieren soll
if not blocking: if not blocking:
self._th_mainloop = Thread( self._th_mainloop = Thread(target=self.mainloop, kwargs={"blocking": True})
target=self.mainloop, kwargs={"blocking": True}
)
self._th_mainloop.start() self._th_mainloop.start()
return return
@@ -1100,17 +1119,17 @@ class RevPiModIO(object):
if not regfunc.prefire: if not regfunc.prefire:
continue continue
if regfunc.edge == BOTH \ if (
or regfunc.edge == RISING and io.value \ regfunc.edge == BOTH
or regfunc.edge == FALLING and not io.value: or regfunc.edge == RISING
and io.value
or regfunc.edge == FALLING
and not io.value
):
if regfunc.as_thread: if regfunc.as_thread:
self._imgwriter._eventqth.put( self._imgwriter._eventqth.put((regfunc, io._name, io.value), False)
(regfunc, io._name, io.value), False
)
else: else:
self._imgwriter._eventq.put( self._imgwriter._eventq.put((regfunc, io._name, io.value), False)
(regfunc, io._name, io.value), False
)
# ImgWriter mit Eventüberwachung aktivieren # ImgWriter mit Eventüberwachung aktivieren
self._imgwriter._collect_events(True) self._imgwriter._collect_events(True)
@@ -1118,7 +1137,6 @@ class RevPiModIO(object):
runtime = -1 if self._debug == -1 else 0 runtime = -1 if self._debug == -1 else 0
while not self._exit.is_set(): while not self._exit.is_set():
# Laufzeit der Eventqueue auf 0 setzen # Laufzeit der Eventqueue auf 0 setzen
if self._imgwriter._eventq.qsize() == 0: if self._imgwriter._eventq.qsize() == 0:
runtime = -1 if self._debug == -1 else 0 runtime = -1 if self._debug == -1 else 0
@@ -1135,13 +1153,12 @@ class RevPiModIO(object):
self._imgwriter._eventq.task_done() self._imgwriter._eventq.task_done()
# Laufzeitprüfung # Laufzeitprüfung
if runtime != -1 and \ if runtime != -1 and default_timer() - runtime > self._imgwriter._refresh:
default_timer() - runtime > self._imgwriter._refresh:
runtime = -1 runtime = -1
warnings.warn( warnings.warn(
"can not execute all event functions in one cycle - " "can not execute all event functions in one cycle - "
"optimize your event functions or rise .cycletime", "optimize your event functions or rise .cycletime",
RuntimeWarning RuntimeWarning,
) )
except Empty: except Empty:
if not self._exit.is_set() and not self._imgwriter.is_alive(): if not self._exit.is_set() and not self._imgwriter.is_alive():
@@ -1177,8 +1194,11 @@ class RevPiModIO(object):
if device is None: if device is None:
mylist = self.device mylist = self.device
else: else:
dev = device if isinstance(device, devicemodule.Device) \ dev = (
device
if isinstance(device, devicemodule.Device)
else self.device.__getitem__(device) else self.device.__getitem__(device)
)
if dev._selfupdate: if dev._selfupdate:
raise RuntimeError( raise RuntimeError(
@@ -1200,7 +1220,6 @@ class RevPiModIO(object):
for dev in mylist: for dev in mylist:
if not dev._selfupdate: if not dev._selfupdate:
# FileHandler sperren # FileHandler sperren
dev._filelock.acquire() dev._filelock.acquire()
@@ -1226,16 +1245,16 @@ class RevPiModIO(object):
:param device: nur auf einzelnes Device anwenden :param device: nur auf einzelnes Device anwenden
""" """
if self._monitoring: if self._monitoring:
raise RuntimeError( raise RuntimeError("can not set default values, while system is in monitoring mode")
"can not set default values, while system is in monitoring "
"mode"
)
if device is None: if device is None:
mylist = self.device mylist = self.device
else: else:
dev = device if isinstance(device, devicemodule.Device) \ dev = (
device
if isinstance(device, devicemodule.Device)
else self.device.__getitem__(device) else self.device.__getitem__(device)
)
mylist = [dev] mylist = [dev]
for dev in mylist: for dev in mylist:
@@ -1254,8 +1273,11 @@ class RevPiModIO(object):
if device is None: if device is None:
mylist = self.device mylist = self.device
else: else:
dev = device if isinstance(device, devicemodule.Device) \ dev = (
device
if isinstance(device, devicemodule.Device)
else self.device.__getitem__(device) else self.device.__getitem__(device)
)
if dev._selfupdate: if dev._selfupdate:
raise RuntimeError( raise RuntimeError(
@@ -1292,16 +1314,16 @@ class RevPiModIO(object):
:return: True, wenn Arbeiten an allen Devices erfolgreich waren :return: True, wenn Arbeiten an allen Devices erfolgreich waren
""" """
if self._monitoring: if self._monitoring:
raise RuntimeError( raise RuntimeError("can not write process image, while system is in monitoring mode")
"can not write process image, while system is in monitoring "
"mode"
)
if device is None: if device is None:
mylist = self.device mylist = self.device
else: else:
dev = device if isinstance(device, devicemodule.Device) \ dev = (
device
if isinstance(device, devicemodule.Device)
else self.device.__getitem__(device) else self.device.__getitem__(device)
)
if dev._selfupdate: if dev._selfupdate:
raise RuntimeError( raise RuntimeError(
@@ -1321,9 +1343,7 @@ class RevPiModIO(object):
if dev._shared_procimg: if dev._shared_procimg:
for io in dev._shared_write: for io in dev._shared_write:
if not io._write_to_procimg(): if not io._write_to_procimg():
global_ex = IOError( global_ex = IOError("error on shared procimg while write")
"error on shared procimg while write"
)
dev._shared_write.clear() dev._shared_write.clear()
else: else:
# Outpus auf Bus schreiben # Outpus auf Bus schreiben
@@ -1375,10 +1395,18 @@ class RevPiModIOSelected(RevPiModIO):
__slots__ = () __slots__ = ()
def __init__( def __init__(
self, deviceselection, autorefresh=False, monitoring=False, self,
syncoutputs=True, procimg=None, configrsc=None, deviceselection,
simulator=False, debug=True, replace_io_file=None, autorefresh=False,
shared_procimg=False, direct_output=False): monitoring=False,
syncoutputs=True,
procimg=None,
configrsc=None,
simulator=False,
debug=True,
replace_io_file=None,
shared_procimg=False,
):
""" """
Instantiiert nur fuer angegebene Devices die Grundfunktionen. Instantiiert nur fuer angegebene Devices die Grundfunktionen.
@@ -1390,8 +1418,15 @@ class RevPiModIOSelected(RevPiModIO):
:ref: :func:`RevPiModIO.__init__(...)` :ref: :func:`RevPiModIO.__init__(...)`
""" """
super().__init__( super().__init__(
autorefresh, monitoring, syncoutputs, procimg, configrsc, autorefresh,
simulator, debug, replace_io_file, shared_procimg, direct_output monitoring,
syncoutputs,
procimg,
configrsc,
simulator,
debug,
replace_io_file,
shared_procimg,
) )
if type(deviceselection) is not DevSelect: if type(deviceselection) is not DevSelect:
@@ -1410,24 +1445,19 @@ class RevPiModIOSelected(RevPiModIO):
if len(self.device) == 0: if len(self.device) == 0:
if self._devselect.type: if self._devselect.type:
raise DeviceNotFoundError( raise DeviceNotFoundError(
"could not find ANY given {0} devices in config" "could not find ANY given {0} devices in config".format(self._devselect.type)
"".format(self._devselect.type)
) )
else: else:
raise DeviceNotFoundError( raise DeviceNotFoundError("could not find ANY given devices in config")
"could not find ANY given devices in config" elif not self._devselect.other_device_key and len(self.device) != len(
) self._devselect.values
elif not self._devselect.other_device_key \ ):
and len(self.device) != len(self._devselect.values):
if self._devselect.type: if self._devselect.type:
raise DeviceNotFoundError( raise DeviceNotFoundError(
"could not find ALL given {0} devices in config" "could not find ALL given {0} devices in config".format(self._devselect.type)
"".format(self._devselect.type)
) )
else: else:
raise DeviceNotFoundError( raise DeviceNotFoundError("could not find ALL given devices in config")
"could not find ALL given devices in config"
)
class RevPiModIODriver(RevPiModIOSelected): class RevPiModIODriver(RevPiModIOSelected):
@@ -1443,9 +1473,16 @@ class RevPiModIODriver(RevPiModIOSelected):
__slots__ = () __slots__ = ()
def __init__( def __init__(
self, virtdev, autorefresh=False, self,
syncoutputs=True, procimg=None, configrsc=None, debug=True, virtdev,
replace_io_file=None, shared_procimg=False, direct_output=False): autorefresh=False,
syncoutputs=True,
procimg=None,
configrsc=None,
debug=True,
replace_io_file=None,
shared_procimg=False,
):
""" """
Instantiiert die Grundfunktionen. Instantiiert die Grundfunktionen.
@@ -1460,14 +1497,20 @@ class RevPiModIODriver(RevPiModIOSelected):
virtdev = (virtdev,) virtdev = (virtdev,)
dev_select = DevSelect(DeviceType.VIRTUAL, "", virtdev) dev_select = DevSelect(DeviceType.VIRTUAL, "", virtdev)
super().__init__( super().__init__(
dev_select, autorefresh, False, syncoutputs, procimg, configrsc, dev_select,
True, debug, replace_io_file, shared_procimg, direct_output autorefresh,
False,
syncoutputs,
procimg,
configrsc,
True,
debug,
replace_io_file,
shared_procimg,
) )
def run_plc( def run_plc(func, cycletime=50, replace_io_file=None, debug=True, procimg=None, configrsc=None):
func, cycletime=50, replace_io_file=None, debug=True,
procimg=None, configrsc=None):
""" """
Run Revoluton Pi as real plc with cycle loop and exclusive IO access. Run Revoluton Pi as real plc with cycle loop and exclusive IO access.

View File

@@ -18,22 +18,22 @@ from .modio import DevSelect, RevPiModIO as _RevPiModIO
from .pictory import DeviceType from .pictory import DeviceType
# Synchronisierungsbefehl # Synchronisierungsbefehl
_syssync = b'\x01\x06\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17' _syssync = b"\x01\x06\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17"
# Disconnectbefehl # Disconnectbefehl
_sysexit = b'\x01EX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17' _sysexit = b"\x01EX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17"
# DirtyBytes von Server entfernen # DirtyBytes von Server entfernen
_sysdeldirty = b'\x01EY\x00\x00\x00\x00\xFF\x00\x00\x00\x00\x00\x00\x00\x17' _sysdeldirty = b"\x01EY\x00\x00\x00\x00\xFF\x00\x00\x00\x00\x00\x00\x00\x17"
# piCtory Konfiguration laden # piCtory Konfiguration laden
_syspictory = b'\x01PI\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17' _syspictory = b"\x01PI\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17"
_syspictoryh = b'\x01PH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17' _syspictoryh = b"\x01PH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17"
# ReplaceIO Konfiguration laden # ReplaceIO Konfiguration laden
_sysreplaceio = b'\x01RP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17' _sysreplaceio = b"\x01RP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17"
_sysreplaceioh = b'\x01RH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17' _sysreplaceioh = b"\x01RH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17"
# Hashvalues # Hashvalues
HASH_FAIL = b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' HASH_FAIL = b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
# Header start/stop # Header start/stop
HEADER_START = b'\x01' HEADER_START = b"\x01"
HEADER_STOP = b'\x17' HEADER_STOP = b"\x17"
class AclException(Exception): class AclException(Exception):
@@ -57,12 +57,28 @@ class NetFH(Thread):
so gesteuert werden. so gesteuert werden.
""" """
__slots__ = "__buff_size", "__buff_block", "__buff_recv", \ __slots__ = (
"__by_buff", "__check_replace_ios", "__config_changed", \ "__buff_size",
"__int_buff", "__dictdirty", "__flusherr", "__replace_ios_h", \ "__buff_block",
"__pictory_h", "__position", "__sockerr", "__sockend", \ "__buff_recv",
"__socklock", "__timeout", "__waitsync", "_address", \ "__by_buff",
"_serversock", "daemon" "__check_replace_ios",
"__config_changed",
"__int_buff",
"__dictdirty",
"__flusherr",
"__replace_ios_h",
"__pictory_h",
"__position",
"__sockerr",
"__sockend",
"__socklock",
"__timeout",
"__waitsync",
"_address",
"_serversock",
"daemon",
)
def __init__(self, address: tuple, check_replace_ios: bool, timeout=500): def __init__(self, address: tuple, check_replace_ios: bool, timeout=500):
""" """
@@ -83,8 +99,8 @@ class NetFH(Thread):
self.__config_changed = False self.__config_changed = False
self.__int_buff = 0 self.__int_buff = 0
self.__dictdirty = {} self.__dictdirty = {}
self.__replace_ios_h = b'' self.__replace_ios_h = b""
self.__pictory_h = b'' self.__pictory_h = b""
self.__sockerr = Event() self.__sockerr = Event()
self.__sockend = Event() self.__sockend = Event()
self.__socklock = Lock() self.__socklock = Lock()
@@ -95,9 +111,7 @@ class NetFH(Thread):
# Parameterprüfung # Parameterprüfung
if not isinstance(address, tuple): if not isinstance(address, tuple):
raise TypeError( raise TypeError("parameter address must be <class 'tuple'> ('IP', PORT)")
"parameter address must be <class 'tuple'> ('IP', PORT)"
)
if not isinstance(timeout, int): if not isinstance(timeout, int):
raise TypeError("parameter timeout must be <class 'int'>") raise TypeError("parameter timeout must be <class 'int'>")
@@ -125,7 +139,7 @@ class NetFH(Thread):
:param bytecode: Antwort, die geprueft werden solll :param bytecode: Antwort, die geprueft werden solll
""" """
if bytecode == b'\x18': if bytecode == b"\x18":
# Alles beenden, wenn nicht erlaubt # Alles beenden, wenn nicht erlaubt
self.__sockend.set() self.__sockend.set()
self.__sockerr.set() self.__sockerr.set()
@@ -174,7 +188,7 @@ class NetFH(Thread):
buff_recv = bytearray(recv_len) buff_recv = bytearray(recv_len)
while recv_len > 0: while recv_len > 0:
block = so.recv(recv_len) block = so.recv(recv_len)
if block == b'': if block == b"":
raise OSError("lost connection on hash receive") raise OSError("lost connection on hash receive")
buff_recv += block buff_recv += block
recv_len -= len(block) recv_len -= len(block)
@@ -183,18 +197,19 @@ class NetFH(Thread):
if self.__pictory_h and buff_recv[:16] != self.__pictory_h: if self.__pictory_h and buff_recv[:16] != self.__pictory_h:
self.__config_changed = True self.__config_changed = True
self.close() self.close()
raise ConfigChanged( raise ConfigChanged("configuration on revolution pi was changed")
"configuration on revolution pi was changed")
else: else:
self.__pictory_h = buff_recv[:16] self.__pictory_h = buff_recv[:16]
# Änderung an replace_ios prüfen # Änderung an replace_ios prüfen
if self.__check_replace_ios and self.__replace_ios_h \ if (
and buff_recv[16:] != self.__replace_ios_h: self.__check_replace_ios
and self.__replace_ios_h
and buff_recv[16:] != self.__replace_ios_h
):
self.__config_changed = True self.__config_changed = True
self.close() self.close()
raise ConfigChanged( raise ConfigChanged("configuration on revolution pi was changed")
"configuration on revolution pi was changed")
else: else:
self.__replace_ios_h = buff_recv[16:] self.__replace_ios_h = buff_recv[16:]
except ConfigChanged: except ConfigChanged:
@@ -295,11 +310,18 @@ class NetFH(Thread):
else: else:
# Nur bestimmte Dirtybytes löschen # Nur bestimmte Dirtybytes löschen
# b CM ii xx c0000000 b = 16 # b CM ii xx c0000000 b = 16
buff = self._direct_sr(pack( buff = self._direct_sr(
pack(
"=c2sH2xc7xc", "=c2sH2xc7xc",
HEADER_START, b'EY', position, b'\xfe', HEADER_STOP HEADER_START,
), 1) b"EY",
if buff != b'\x1e': position,
b"\xfe",
HEADER_STOP,
),
1,
)
if buff != b"\x1e":
# ACL prüfen und ggf Fehler werfen # ACL prüfen und ggf Fehler werfen
self.__check_acl(buff) self.__check_acl(buff)
@@ -343,12 +365,18 @@ class NetFH(Thread):
try: try:
# b CM ii ii 00000000 b = 16 # b CM ii ii 00000000 b = 16
buff = self._direct_sr(pack( buff = self._direct_sr(
pack(
"=c2sHH8xc", "=c2sHH8xc",
HEADER_START, HEADER_START,
b'FD', self.__int_buff, len(self.__by_buff), b"FD",
HEADER_STOP self.__int_buff,
) + self.__by_buff, 1) len(self.__by_buff),
HEADER_STOP,
)
+ self.__by_buff,
1,
)
except Exception: except Exception:
raise raise
finally: finally:
@@ -356,7 +384,7 @@ class NetFH(Thread):
self.__int_buff = 0 self.__int_buff = 0
self.__by_buff.clear() self.__by_buff.clear()
if buff != b'\x1e': if buff != b"\x1e":
# ACL prüfen und ggf Fehler werfen # ACL prüfen und ggf Fehler werfen
self.__check_acl(buff) self.__check_acl(buff)
@@ -403,7 +431,7 @@ class NetFH(Thread):
""" """
return int(self.__timeout * 1000) return int(self.__timeout * 1000)
def ioctl(self, request: int, arg=b'') -> None: def ioctl(self, request: int, arg=b"") -> None:
""" """
IOCTL Befehle ueber das Netzwerk senden. IOCTL Befehle ueber das Netzwerk senden.
@@ -419,11 +447,10 @@ class NetFH(Thread):
raise TypeError("arg must be <class 'bytes'>") raise TypeError("arg must be <class 'bytes'>")
# b CM xx ii iiii0000 b = 16 # b CM xx ii iiii0000 b = 16
buff = self._direct_sr(pack( buff = self._direct_sr(
"=c2s2xHI4xc", pack("=c2s2xHI4xc", HEADER_START, b"IC", len(arg), request, HEADER_STOP) + arg, 1
HEADER_START, b'IC', len(arg), request, HEADER_STOP )
) + arg, 1) if buff != b"\x1e":
if buff != b'\x1e':
# ACL prüfen und ggf Fehler werfen # ACL prüfen und ggf Fehler werfen
self.__check_acl(buff) self.__check_acl(buff)
@@ -443,10 +470,9 @@ class NetFH(Thread):
raise ValueError("read of closed file") raise ValueError("read of closed file")
# b CM ii ii 00000000 b = 16 # b CM ii ii 00000000 b = 16
buff = self._direct_sr(pack( buff = self._direct_sr(
"=c2sHH8xc", pack("=c2sHH8xc", HEADER_START, b"DA", self.__position, length, HEADER_STOP), length
HEADER_START, b'DA', self.__position, length, HEADER_STOP )
), length)
self.__position += length self.__position += length
return buff return buff
@@ -466,10 +492,9 @@ class NetFH(Thread):
length = len(buffer) length = len(buffer)
# b CM ii ii 00000000 b = 16 # b CM ii ii 00000000 b = 16
buff = self._direct_sr(pack( buff = self._direct_sr(
"=c2sHH8xc", pack("=c2sHH8xc", HEADER_START, b"DA", self.__position, length, HEADER_STOP), length
HEADER_START, b'DA', self.__position, length, HEADER_STOP )
), length)
buffer[:] = buff buffer[:] = buff
return len(buffer) return len(buffer)
@@ -484,13 +509,11 @@ class NetFH(Thread):
raise ValueError("read of closed file") raise ValueError("read of closed file")
if self.__pictory_h == HASH_FAIL: if self.__pictory_h == HASH_FAIL:
raise RuntimeError( raise RuntimeError("could not read/parse piCtory configuration over network")
"could not read/parse piCtory configuration over network"
)
buff = self._direct_sr(_syspictory, 4) buff = self._direct_sr(_syspictory, 4)
recv_length, = unpack("=I", buff) (recv_length,) = unpack("=I", buff)
return self._direct_sr(b'', recv_length) return self._direct_sr(b"", recv_length)
def readreplaceio(self) -> bytes: def readreplaceio(self) -> bytes:
""" """
@@ -502,27 +525,21 @@ class NetFH(Thread):
raise ValueError("read of closed file") raise ValueError("read of closed file")
if self.__replace_ios_h == HASH_FAIL: if self.__replace_ios_h == HASH_FAIL:
raise RuntimeError( raise RuntimeError("replace_io_file: could not read/parse over network")
"replace_io_file: could not read/parse over network"
)
buff = self._direct_sr(_sysreplaceio, 4) buff = self._direct_sr(_sysreplaceio, 4)
recv_length, = unpack("=I", buff) (recv_length,) = unpack("=I", buff)
return self._direct_sr(b'', recv_length) return self._direct_sr(b"", recv_length)
def run(self) -> None: def run(self) -> None:
"""Handler fuer Synchronisierung.""" """Handler fuer Synchronisierung."""
state_reconnect = False state_reconnect = False
while not self.__sockend.is_set(): while not self.__sockend.is_set():
# Bei Fehlermeldung neu verbinden # Bei Fehlermeldung neu verbinden
if self.__sockerr.is_set(): if self.__sockerr.is_set():
if not state_reconnect: if not state_reconnect:
state_reconnect = True state_reconnect = True
warnings.warn( warnings.warn("got a network error and try to reconnect", RuntimeWarning)
"got a network error and try to reconnect",
RuntimeWarning
)
self._connect() self._connect()
if self.__sockerr.is_set(): if self.__sockerr.is_set():
# Verhindert beim Scheitern 100% CPU last # Verhindert beim Scheitern 100% CPU last
@@ -530,10 +547,7 @@ class NetFH(Thread):
continue continue
else: else:
state_reconnect = False state_reconnect = False
warnings.warn( warnings.warn("successfully reconnected after network error", RuntimeWarning)
"successfully reconnected after network error",
RuntimeWarning
)
# Kein Fehler aufgetreten, sync durchführen wenn socket frei # Kein Fehler aufgetreten, sync durchführen wenn socket frei
if self.__socklock.acquire(blocking=False): if self.__socklock.acquire(blocking=False):
@@ -543,9 +557,7 @@ class NetFH(Thread):
self.__buff_recv.clear() self.__buff_recv.clear()
recv_lenght = 2 recv_lenght = 2
while recv_lenght > 0: while recv_lenght > 0:
count = self._serversock.recv_into( count = self._serversock.recv_into(self.__buff_block, recv_lenght)
self.__buff_block, recv_lenght
)
if count == 0: if count == 0:
raise IOError("lost network connection on sync") raise IOError("lost network connection on sync")
self.__buff_recv += self.__buff_block[:count] self.__buff_recv += self.__buff_block[:count]
@@ -554,11 +566,8 @@ class NetFH(Thread):
except IOError: except IOError:
self.__sockerr.set() self.__sockerr.set()
else: else:
if self.__buff_recv != b'\x06\x16': if self.__buff_recv != b"\x06\x16":
warnings.warn( warnings.warn("data error on network sync", RuntimeWarning)
"data error on network sync",
RuntimeWarning
)
self.__sockerr.set() self.__sockerr.set()
continue continue
finally: finally:
@@ -596,12 +605,13 @@ class NetFH(Thread):
try: try:
# b CM ii ii 00000000 b = 16 # b CM ii ii 00000000 b = 16
buff = self._direct_sr(pack( buff = self._direct_sr(
"=c2sHH8xc", pack("=c2sHH8xc", HEADER_START, b"EY", position, len(dirtybytes), HEADER_STOP)
HEADER_START, b'EY', position, len(dirtybytes), HEADER_STOP + dirtybytes,
) + dirtybytes, 1) 1,
)
if buff != b'\x1e': if buff != b"\x1e":
# ACL prüfen und ggf Fehler werfen # ACL prüfen und ggf Fehler werfen
self.__check_acl(buff) self.__check_acl(buff)
@@ -627,11 +637,8 @@ class NetFH(Thread):
try: try:
# b CM ii xx 00000000 b = 16 # b CM ii xx 00000000 b = 16
buff = self._direct_sr(pack( buff = self._direct_sr(pack("=c2sH10xc", HEADER_START, b"CF", value, HEADER_STOP), 1)
"=c2sH10xc", if buff != b"\x1e":
HEADER_START, b'CF', value, HEADER_STOP
), 1)
if buff != b'\x1e':
raise IOError("set timeout error on network") raise IOError("set timeout error on network")
except Exception: except Exception:
self.__sockerr.set() self.__sockerr.set()
@@ -666,10 +673,11 @@ class NetFH(Thread):
self.__int_buff += 1 self.__int_buff += 1
# Datenblock mit Position und Länge in Puffer ablegen # Datenblock mit Position und Länge in Puffer ablegen
self.__by_buff += \ self.__by_buff += (
self.__position.to_bytes(length=2, byteorder="little") \ self.__position.to_bytes(length=2, byteorder="little")
+ len(bytebuff).to_bytes(length=2, byteorder="little") \ + len(bytebuff).to_bytes(length=2, byteorder="little")
+ bytebuff + bytebuff
)
# TODO: Bufferlänge und dann flushen? # TODO: Bufferlänge und dann flushen?
@@ -697,9 +705,16 @@ class RevPiNetIO(_RevPiModIO):
__slots__ = "_address" __slots__ = "_address"
def __init__( def __init__(
self, address, autorefresh=False, monitoring=False, self,
syncoutputs=True, simulator=False, debug=True, address,
replace_io_file=None, shared_procimg=False, direct_output=False): autorefresh=False,
monitoring=False,
syncoutputs=True,
simulator=False,
debug=True,
replace_io_file=None,
shared_procimg=False,
):
""" """
Instantiiert die Grundfunktionen. Instantiiert die Grundfunktionen.
@@ -712,30 +727,21 @@ class RevPiNetIO(_RevPiModIO):
:param replace_io_file: Replace IO Konfiguration aus Datei laden :param replace_io_file: Replace IO Konfiguration aus Datei laden
:param shared_procimg: Share process image with other processes, this :param shared_procimg: Share process image with other processes, this
could be insecure for automation could be insecure for automation
:param direct_output: Deprecated, use shared_procimg
""" """
check_ip = compile( check_ip = compile(r"^(25[0-5]|(2[0-4]|[01]?\d|)\d)(\.(25[0-5]|(2[0-4]|[01]?\d|)\d)){3}$")
r"^(25[0-5]|(2[0-4]|[01]?\d|)\d)"
r"(\.(25[0-5]|(2[0-4]|[01]?\d|)\d)){3}$"
)
# Adresse verarbeiten # Adresse verarbeiten
if isinstance(address, str): if isinstance(address, str):
self._address = (address, 55234) self._address = (address, 55234)
elif isinstance(address, tuple): elif isinstance(address, tuple):
if len(address) == 2 \ if len(address) == 2 and isinstance(address[0], str) and isinstance(address[1], int):
and isinstance(address[0], str) \
and isinstance(address[1], int):
# Werte prüfen # Werte prüfen
if not 0 < address[1] <= 65535: if not 0 < address[1] <= 65535:
raise ValueError("port number out of range 1 - 65535") raise ValueError("port number out of range 1 - 65535")
self._address = address self._address = address
else: else:
raise TypeError( raise TypeError("address tuple must be (<class 'str'>, <class 'int'>)")
"address tuple must be (<class 'str'>, <class 'int'>)"
)
else: else:
raise TypeError( raise TypeError(
"parameter address must be <class 'str'> or <class 'tuple'> " "parameter address must be <class 'str'> or <class 'tuple'> "
@@ -749,8 +755,7 @@ class RevPiNetIO(_RevPiModIO):
self._address = (ipv4, self._address[1]) self._address = (ipv4, self._address[1])
except Exception: except Exception:
raise ValueError( raise ValueError(
"can not resolve ip address for hostname '{0}'" "can not resolve ip address for hostname '{0}'".format(self._address[0])
"".format(self._address[0])
) )
# Vererben # Vererben
@@ -764,7 +769,6 @@ class RevPiNetIO(_RevPiModIO):
debug=debug, debug=debug,
replace_io_file=replace_io_file, replace_io_file=replace_io_file,
shared_procimg=shared_procimg, shared_procimg=shared_procimg,
direct_output=direct_output,
) )
self._set_device_based_cycle_time = False self._set_device_based_cycle_time = False
@@ -801,10 +805,7 @@ class RevPiNetIO(_RevPiModIO):
try: try:
cp.read_string(byte_buff.decode("utf-8")) cp.read_string(byte_buff.decode("utf-8"))
except Exception as e: except Exception as e:
raise RuntimeError( raise RuntimeError("replace_io_file: could not read/parse network data | {0}".format(e))
"replace_io_file: could not read/parse network data | {0}"
"".format(e)
)
return cp return cp
def disconnect(self) -> None: def disconnect(self) -> None:
@@ -862,16 +863,12 @@ class RevPiNetIO(_RevPiModIO):
:param device: nur auf einzelnes Device anwenden, sonst auf Alle :param device: nur auf einzelnes Device anwenden, sonst auf Alle
""" """
if self.monitoring: if self.monitoring:
raise RuntimeError( raise RuntimeError("can not send default values, while system is in monitoring mode")
"can not send default values, while system is in "
"monitoring mode"
)
if device is None: if device is None:
self._myfh.clear_dirtybytes() self._myfh.clear_dirtybytes()
else: else:
dev = device if isinstance(device, Device) \ dev = device if isinstance(device, Device) else self.device.__getitem__(device)
else self.device.__getitem__(device)
mylist = [dev] mylist = [dev]
for dev in mylist: for dev in mylist:
@@ -887,16 +884,12 @@ class RevPiNetIO(_RevPiModIO):
:param device: nur auf einzelnes Device anwenden, sonst auf Alle :param device: nur auf einzelnes Device anwenden, sonst auf Alle
""" """
if self.monitoring: if self.monitoring:
raise RuntimeError( raise RuntimeError("can not send default values, while system is in monitoring mode")
"can not send default values, while system is in "
"monitoring mode"
)
if device is None: if device is None:
mylist = self.device mylist = self.device
else: else:
dev = device if isinstance(device, Device) \ dev = device if isinstance(device, Device) else self.device.__getitem__(device)
else self.device.__getitem__(device)
mylist = [dev] mylist = [dev]
for dev in mylist: for dev in mylist:
@@ -921,13 +914,10 @@ class RevPiNetIO(_RevPiModIO):
int_byte += 1 if bitio._defaultvalue else 0 int_byte += 1 if bitio._defaultvalue else 0
# Errechneten Int-Wert in ein Byte umwandeln # Errechneten Int-Wert in ein Byte umwandeln
dirtybytes += \ dirtybytes += int_byte.to_bytes(length=1, byteorder="little")
int_byte.to_bytes(length=1, byteorder="little")
# Dirtybytes an PLC Server senden # Dirtybytes an PLC Server senden
self._myfh.set_dirtybytes( self._myfh.set_dirtybytes(dev._offset + dev._slc_out.start, dirtybytes)
dev._offset + dev._slc_out.start, dirtybytes
)
config_changed = property(get_config_changed) config_changed = property(get_config_changed)
reconnecting = property(get_reconnecting) reconnecting = property(get_reconnecting)
@@ -946,9 +936,17 @@ class RevPiNetIOSelected(RevPiNetIO):
__slots__ = () __slots__ = ()
def __init__( def __init__(
self, address, deviceselection, autorefresh=False, self,
monitoring=False, syncoutputs=True, simulator=False, debug=True, address,
replace_io_file=None, shared_procimg=False, direct_output=False): deviceselection,
autorefresh=False,
monitoring=False,
syncoutputs=True,
simulator=False,
debug=True,
replace_io_file=None,
shared_procimg=False,
):
""" """
Instantiiert nur fuer angegebene Devices die Grundfunktionen. Instantiiert nur fuer angegebene Devices die Grundfunktionen.
@@ -961,8 +959,14 @@ class RevPiNetIOSelected(RevPiNetIO):
:ref: :func:`RevPiNetIO.__init__()` :ref: :func:`RevPiNetIO.__init__()`
""" """
super().__init__( super().__init__(
address, autorefresh, monitoring, syncoutputs, simulator, debug, address,
replace_io_file, shared_procimg, direct_output autorefresh,
monitoring,
syncoutputs,
simulator,
debug,
replace_io_file,
shared_procimg,
) )
if type(deviceselection) is not DevSelect: if type(deviceselection) is not DevSelect:
@@ -981,24 +985,19 @@ class RevPiNetIOSelected(RevPiNetIO):
if len(self.device) == 0: if len(self.device) == 0:
if self._devselect.type: if self._devselect.type:
raise DeviceNotFoundError( raise DeviceNotFoundError(
"could not find ANY given {0} devices in config" "could not find ANY given {0} devices in config".format(self._devselect.type)
"".format(self._devselect.type)
) )
else: else:
raise DeviceNotFoundError( raise DeviceNotFoundError("could not find ANY given devices in config")
"could not find ANY given devices in config" elif not self._devselect.other_device_key and len(self.device) != len(
) self._devselect.values
elif not self._devselect.other_device_key \ ):
and len(self.device) != len(self._devselect.values):
if self._devselect.type: if self._devselect.type:
raise DeviceNotFoundError( raise DeviceNotFoundError(
"could not find ALL given {0} devices in config" "could not find ALL given {0} devices in config".format(self._devselect.type)
"".format(self._devselect.type)
) )
else: else:
raise DeviceNotFoundError( raise DeviceNotFoundError("could not find ALL given devices in config")
"could not find ALL given devices in config"
)
class RevPiNetIODriver(RevPiNetIOSelected): class RevPiNetIODriver(RevPiNetIOSelected):
@@ -1014,9 +1013,15 @@ class RevPiNetIODriver(RevPiNetIOSelected):
__slots__ = () __slots__ = ()
def __init__( def __init__(
self, address, virtdev, autorefresh=False, self,
syncoutputs=True, debug=True, replace_io_file=None, address,
shared_procimg=False, direct_output=False): virtdev,
autorefresh=False,
syncoutputs=True,
debug=True,
replace_io_file=None,
shared_procimg=False,
):
""" """
Instantiiert die Grundfunktionen. Instantiiert die Grundfunktionen.
@@ -1032,13 +1037,19 @@ class RevPiNetIODriver(RevPiNetIOSelected):
virtdev = (virtdev,) virtdev = (virtdev,)
dev_select = DevSelect(DeviceType.VIRTUAL, "", virtdev) dev_select = DevSelect(DeviceType.VIRTUAL, "", virtdev)
super().__init__( super().__init__(
address, dev_select, autorefresh, False, syncoutputs, True, debug, address,
replace_io_file, shared_procimg, direct_output dev_select,
autorefresh,
False,
syncoutputs,
True,
debug,
replace_io_file,
shared_procimg,
) )
def run_net_plc( def run_net_plc(address, func, cycletime=50, replace_io_file=None, debug=True):
address, func, cycletime=50, replace_io_file=None, debug=True):
""" """
Run Revoluton Pi as real plc with cycle loop and exclusive IO access. Run Revoluton Pi as real plc with cycle loop and exclusive IO access.

View File

@@ -13,7 +13,12 @@ __license__ = "LGPLv2"
# - RevPiConCan_20180425_1_0.rap # - RevPiConCan_20180425_1_0.rap
# - RevPiGateCANopen_20161102_1_0.rap # - RevPiGateCANopen_20161102_1_0.rap
class ProductType: class ProductType:
CON_BT = 111
CON_CAN = 109
CON_MBUS = 110
GATEWAY_CAN_OPEN = 71 GATEWAY_CAN_OPEN = 71
GATEWAY_CCLINK = 72 GATEWAY_CCLINK = 72
GATEWAY_DEV_NET = 73 GATEWAY_DEV_NET = 73
@@ -38,6 +43,7 @@ class ProductType:
DO = 98 DO = 98
AIO = 103 AIO = 103
MIO = 118 MIO = 118
RO = 137
REVPI_CORE = 95 REVPI_CORE = 95
REVPI_COMPACT = 104 REVPI_COMPACT = 104
@@ -45,9 +51,22 @@ class ProductType:
REVPI_FLAT = 135 REVPI_FLAT = 135
REVPI_CONNECT_4 = 136 REVPI_CONNECT_4 = 136
VIRTUAL_CLOUD = 24584
VIRTUAL_MODBUS_TCP_SERVER = 24577
VIRTUAL_MODBUS_RTU_SERVER = 24578
VIRTUAL_MODBUS_TCP_CLIENT = 24579
VIRTUAL_MODBUS_RTU_CLIENT = 24580
VIRTUAL_OPCUA_SERVER = 23001
VIRTUAL_REVPI_SEVEN = 24583
VIRTUAL_PN_CONTROLLER = 24581
VIRTUAL_PN_DEVICE = 24582
VIRTUAL_TIMER = 28673
VIRTUAL_RAW = 32768
class DeviceType: class DeviceType:
"""Module key "type" in piCtory file.""" """Module key "type" in piCtory file."""
IGNORED = "" IGNORED = ""
BASE = "BASE" # Core devices BASE = "BASE" # Core devices
EDGE = "EDGE" # Gateways EDGE = "EDGE" # Gateways
@@ -58,6 +77,7 @@ class DeviceType:
class AIO: class AIO:
"""Memory value mappings for RevPi AIO 1.0 (RevPiAIO_20170301_1_0.rap).""" """Memory value mappings for RevPi AIO 1.0 (RevPiAIO_20170301_1_0.rap)."""
OUT_RANGE_OFF = 0 # Off OUT_RANGE_OFF = 0 # Off
OUT_RANGE_0_5V = 1 # 0 - 5V OUT_RANGE_0_5V = 1 # 0 - 5V
OUT_RANGE_0_10V = 2 # 0 - 10V OUT_RANGE_0_10V = 2 # 0 - 10V
@@ -131,6 +151,7 @@ class AIO:
class DI: class DI:
"""Memory value mappings for RevPi DI 1.0 (RevPiDI_20160818_1_0.rap).""" """Memory value mappings for RevPi DI 1.0 (RevPiDI_20160818_1_0.rap)."""
IN_MODE_DIRECT = 0 # Direct IN_MODE_DIRECT = 0 # Direct
IN_MODE_COUNT_RISING = 1 # Counter, rising edge IN_MODE_COUNT_RISING = 1 # Counter, rising edge
IN_MODE_COUNT_FALLING = 2 # Counter, falling edge IN_MODE_COUNT_FALLING = 2 # Counter, falling edge
@@ -144,6 +165,7 @@ class DI:
class DO: class DO:
"""Memory value mappings for RevPi DO 1.0 (RevPiDO_20160818_1_0.rap).""" """Memory value mappings for RevPi DO 1.0 (RevPiDO_20160818_1_0.rap)."""
OUT_PWM_FREQ_40HZ = 1 # 40Hz 1% OUT_PWM_FREQ_40HZ = 1 # 40Hz 1%
OUT_PWM_FREQ_80HZ = 2 # 80Hz 2% OUT_PWM_FREQ_80HZ = 2 # 80Hz 2%
OUT_PWM_FREQ_160HZ = 4 # 160Hz 4% OUT_PWM_FREQ_160HZ = 4 # 160Hz 4%
@@ -153,11 +175,13 @@ class DO:
class DIO(DI, DO): class DIO(DI, DO):
"""Memory value mappings for RevPi DIO 1.0 (RevPiDIO_20160818_1_0.rap).""" """Memory value mappings for RevPi DIO 1.0 (RevPiDIO_20160818_1_0.rap)."""
pass pass
class MIO: class MIO:
"""Memory value mappings for RevPi MIO 1.0 (RevPiMIO_20200901_1_0.rap).""" """Memory value mappings for RevPi MIO 1.0 (RevPiMIO_20200901_1_0.rap)."""
ENCODER_MODE_DISABLED = 0 ENCODER_MODE_DISABLED = 0
ENCODER_MODE_ENABLED = 1 ENCODER_MODE_ENABLED = 1
@@ -176,6 +200,7 @@ class MIO:
class COMPACT: class COMPACT:
"""Memory value mappings for RevPi Compact 1.0 (RevPiCompact_20171023_1_0.rap).""" """Memory value mappings for RevPi Compact 1.0 (RevPiCompact_20171023_1_0.rap)."""
DIN_DEBOUNCE_OFF = 0 # Off DIN_DEBOUNCE_OFF = 0 # Off
DIN_DEBOUNCE_25US = 1 # 25us DIN_DEBOUNCE_25US = 1 # 25us
DIN_DEBOUNCE_750US = 2 # 750us DIN_DEBOUNCE_750US = 2 # 750us
@@ -189,5 +214,6 @@ class COMPACT:
class FLAT: class FLAT:
"""Memory value mappings for RevPi Flat 1.0 (RevPiFlat_20200921_1_0.rap).""" """Memory value mappings for RevPi Flat 1.0 (RevPiFlat_20200921_1_0.rap)."""
IN_RANGE_0_10V = 0 IN_RANGE_0_10V = 0
IN_RANGE_4_20MA = 1 IN_RANGE_4_20MA = 1

14
tests/test_import.py Normal file
View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
"""Test module import."""
# SPDX-FileCopyrightText: 2023 Sven Sager
# SPDX-License-Identifier: GPL-2.0-or-later
import unittest
class ModuleImport(unittest.TestCase):
def test_import(self):
"""Test the import of the module."""
import revpimodio2
self.assertEqual(type(revpimodio2.__version__), str)