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/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
@@ -46,8 +47,10 @@ htmlcov/
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
@@ -57,6 +60,7 @@ coverage.xml
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
@@ -69,6 +73,7 @@ instance/
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
@@ -79,10 +84,38 @@ profile_default/
ipython_config.py
# 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.pid
# SageMath parsed files
*.sage.py
@@ -114,5 +147,17 @@ dmypy.json
# Pyre type checker
.pyre/
/make.conf
/test_local/
# pytype static type analyzer
.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">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<option name="RIGHT_MARGIN" value="80" />
<option name="RIGHT_MARGIN" value="100" />
</code_scheme>
</component>

6
.idea/misc.xml generated
View File

@@ -1,5 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<option name="languageLevel" value="ES6" />
</component>

View File

@@ -1,221 +1,397 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
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
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
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.
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
When we speak of free software, we are referring to freedom, 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 or use pieces of it
in new free programs; and that you know you can do these things.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
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
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
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
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
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
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
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
modification follow.
GNU GENERAL PUBLIC LICENSE
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
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
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(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
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
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
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy, 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
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
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
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.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
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
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
themselves, then this License, and its terms, do not apply to those
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
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
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
your rights to work written entirely by you; rather, the intent is to
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
with the Program (or with a work based on the Program) on a volume of
In addition, mere aggregation of another work not based on the Library
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
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
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
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
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
If distribution of 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 satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program 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.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
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
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
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
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
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
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.
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),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
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
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
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
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
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
generous contributions to the wide range of software distributed
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
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
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
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
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To apply these terms, attach the following notices to the library. It is
safest 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.
To do so, attach the following notices to the program. It is safest
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.>
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
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
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
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.
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
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:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
Yoyodyne, Inc., hereby disclaims all copyright interest in the
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
This General Public License does not permit incorporating your program into
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.
That's all there is to it!

View File

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

View File

@@ -26,32 +26,56 @@ venv-info:
exit 0
venv:
$(SYSTEM_PYTHON) -m venv "$(VENV_PATH)"
source $(VENV_PATH)/bin/activate && \
# Start with empty environment
"$(SYSTEM_PYTHON)" -m venv "$(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:
# 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:
$(PYTHON) -m setup sdist
$(PYTHON) -m setup bdist_wheel
"$(PYTHON)" -m setup sdist
"$(PYTHON)" -m setup bdist_wheel
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:
$(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:
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
rm -R $(VENV_PATH)
distclean: clean
# 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
wheel
# Build dependencies
pytest-cov
setuptools
sphinx
wheel
# Runtime dependencies, must match install_requires in setup.py

View File

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

View File

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

View File

@@ -5,11 +5,11 @@ __copyright__ = "Copyright (C) 2023 Sven Sager"
__license__ = "LGPLv2"
import struct
import warnings
from re import match as rematch
from threading import Event
from ._internal import consttostr, RISING, FALLING, BOTH, INP, OUT, \
MEM, PROCESS_IMAGE_SIZE
from ._internal import consttostr, RISING, FALLING, BOTH, INP, OUT, MEM, PROCESS_IMAGE_SIZE
try:
# Funktioniert nur auf Unix
@@ -36,10 +36,11 @@ class IOEvent(object):
class IOList(object):
"""Basisklasse fuer direkten Zugriff auf IO Objekte."""
def __init__(self):
def __init__(self, modio):
"""Init IOList class."""
self.__dict_iobyte = {k: [] for k in range(PROCESS_IMAGE_SIZE)}
self.__dict_iorefname = {}
self.__modio = modio
def __contains__(self, key):
"""
@@ -69,8 +70,16 @@ class IOList(object):
self.__dict_iobyte[io_del.address][io_del._bitaddress] = None
# Do not use any() because we want to know None, not 0
if self.__dict_iobyte[io_del.address] == \
[None, None, None, None, None, None, None, None]:
if self.__dict_iobyte[io_del.address] == [
None,
None,
None,
None,
None,
None,
None,
None,
]:
self.__dict_iobyte[io_del.address] = []
else:
self.__dict_iobyte[io_del.address].remove(io_del)
@@ -78,6 +87,44 @@ class IOList(object):
object.__delattr__(self, key)
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):
"""
Verwaltet geloeschte IOs (Attribute, die nicht existieren).
@@ -110,9 +157,7 @@ class IOList(object):
elif type(key) == slice:
return [
self.__dict_iobyte[int_io]
for int_io in range(
key.start, key.stop, 1 if key.step is None else key.step
)
for int_io in range(key.start, key.stop, 1 if key.step is None else key.step)
]
else:
return getattr(self, key)
@@ -144,14 +189,13 @@ class IOList(object):
def __setattr__(self, key, value):
"""Verbietet aus Leistungsguenden das direkte Setzen von Attributen."""
if key in (
"_IOList__dict_iobyte",
"_IOList__dict_iorefname"
"_IOList__dict_iobyte",
"_IOList__dict_iorefname",
"_IOList__modio",
):
object.__setattr__(self, key, value)
else:
raise AttributeError(
"direct assignment is not supported - use .value Attribute"
)
raise AttributeError("direct assignment is not supported - use .value Attribute")
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)
# Defaultvalue über mehrere Bytes sammeln
calc_defaultvalue = b''
calc_defaultvalue = b""
for i in range(scan_start, scan_stop):
for oldio in self.__dict_iobyte[i]:
if type(oldio) == StructIO:
# Hier gibt es schon einen neuen IO
if oldio._bitshift:
if io._bitshift == oldio._bitshift \
and io._slc_address == oldio._slc_address:
if (
io._bitshift == oldio._bitshift
and io._slc_address == oldio._slc_address
):
raise MemoryError(
"bit {0} already assigned to '{1}'".format(
io._bitaddress, oldio._name
@@ -186,9 +231,7 @@ class IOList(object):
else:
# Bereits überschriebene bytes sind ungültig
raise MemoryError(
"new io '{0}' overlaps memory of '{1}'".format(
io._name, oldio._name
)
"new io '{0}' overlaps memory of '{1}'".format(io._name, oldio._name)
)
elif oldio is not None:
# IOs im Speicherbereich des neuen IO merken
@@ -201,8 +244,7 @@ class IOList(object):
if io._byteorder == "little":
calc_defaultvalue += oldio._defaultvalue
else:
calc_defaultvalue = \
oldio._defaultvalue + calc_defaultvalue
calc_defaultvalue = oldio._defaultvalue + calc_defaultvalue
# ios aus listen entfernen
delattr(self, oldio._name)
@@ -211,9 +253,7 @@ class IOList(object):
# Nur bei StructIO und keiner gegebenen defaultvalue übernehmen
if io._bitshift:
io_byte_address = io._parentio_address - io.address
io._defaultvalue = bool(
io._parentio_defaultvalue[io_byte_address] & io._bitshift
)
io._defaultvalue = bool(io._parentio_defaultvalue[io_byte_address] & io._bitshift)
else:
io._defaultvalue = calc_defaultvalue
@@ -226,25 +266,75 @@ class IOList(object):
if isinstance(new_io, IOBase):
if hasattr(self, new_io._name):
raise AttributeError(
"attribute {0} already exists - can not set io"
"".format(new_io._name)
"attribute {0} already exists - can not set io".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)
object.__setattr__(self, new_io._name, new_io)
# Bytedict für Adresszugriff anpassen
if new_io._bitshift:
if len(self.__dict_iobyte[new_io.address]) != 8:
# "schnell" 8 Einträge erstellen da es BIT IOs sind
self.__dict_iobyte[new_io.address] += \
[None, None, None, None, None, None, None, None]
self.__dict_iobyte[new_io.address] += [
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
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)
object.__setattr__(self, new_io._name, new_io)
if type(new_io) is StructIO:
new_io._parentdevice._update_my_io_list()
else:
@@ -289,13 +379,26 @@ class IOBase(object):
auch als <class 'int'> verwendet werden koennen.
"""
__slots__ = "__bit_ioctl_off", "__bit_ioctl_on", "_bitaddress", \
"_bitshift", "_bitlength", "_byteorder", "_defaultvalue", \
"_export", "_iotype", "_length", "_name", "_parentdevice", \
"_read_only_io", "_signed", "_slc_address", "bmk"
__slots__ = (
"__bit_ioctl_off",
"__bit_ioctl_on",
"_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,
byteorder: str, signed: bool):
def __init__(self, parentdevice, valuelist: list, iotype: int, byteorder: str, signed: bool):
"""
Instantiierung der IOBase-Klasse.
@@ -312,8 +415,7 @@ class IOBase(object):
# Bitadressen auf Bytes aufbrechen und umrechnen
self._bitaddress = -1 if valuelist[7] == "" else int(valuelist[7]) % 8
self._bitshift = None if self._bitaddress == -1 \
else 1 << self._bitaddress
self._bitshift = None if self._bitaddress == -1 else 1 << self._bitaddress
# Längenberechnung
self._bitlength = int(valuelist[2])
@@ -333,9 +435,7 @@ class IOBase(object):
if self._bitshift:
# Höhere Bits als 7 auf nächste Bytes umbrechen
int_startaddress += int(int(valuelist[7]) / 8)
self._slc_address = slice(
int_startaddress, int_startaddress + 1
)
self._slc_address = slice(int_startaddress, int_startaddress + 1)
# Defaultvalue ermitteln, sonst False
if valuelist[1] is None and type(self) == StructIO:
@@ -347,14 +447,10 @@ class IOBase(object):
self._defaultvalue = False
# Ioctl für Bitsetzung setzen
self.__bit_ioctl_off = struct.pack(
"<HB", self._get_address(), self._bitaddress
)
self.__bit_ioctl_on = self.__bit_ioctl_off + b'\x01'
self.__bit_ioctl_off = struct.pack("<HB", self._get_address(), self._bitaddress)
self.__bit_ioctl_on = self.__bit_ioctl_off + b"\x01"
else:
self._slc_address = slice(
int_startaddress, int_startaddress + self._length
)
self._slc_address = slice(int_startaddress, int_startaddress + self._length)
if str(valuelist[1]).isdigit():
# Defaultvalue aus Zahl in Bytes umrechnen
self._defaultvalue = int(valuelist[1]).to_bytes(
@@ -382,8 +478,7 @@ class IOBase(object):
try:
buff = valuelist[1].encode("ASCII")
if len(buff) <= self._length:
self._defaultvalue = \
buff + bytes(self._length - len(buff))
self._defaultvalue = buff + bytes(self._length - len(buff))
except Exception:
pass
@@ -394,10 +489,7 @@ class IOBase(object):
:return: <class 'bool'> Nur False wenn False oder 0 sonst True
"""
if self._bitshift:
return bool(
self._parentdevice._ba_devdata[self._slc_address.start]
& self._bitshift
)
return bool(self._parentdevice._ba_devdata[self._slc_address.start] & self._bitshift)
else:
return any(self._parentdevice._ba_devdata[self._slc_address])
@@ -406,8 +498,7 @@ class IOBase(object):
# Inline get_value()
if self._bitshift:
return bool(
self._parentdevice._ba_devdata[self._slc_address.start]
& self._bitshift
self._parentdevice._ba_devdata[self._slc_address.start] & self._bitshift
)
else:
return bytes(self._parentdevice._ba_devdata[self._slc_address])
@@ -430,8 +521,9 @@ class IOBase(object):
"""
return self._name
def __reg_xevent(self, func, delay: int, edge: int, as_thread: bool,
overwrite: bool, prefire: bool) -> None:
def __reg_xevent(
self, func, delay: int, edge: int, as_thread: bool, overwrite: bool, prefire: bool
) -> None:
"""
Verwaltet reg_event und reg_timerevent.
@@ -444,26 +536,19 @@ class IOBase(object):
"""
# Prüfen ob Funktion callable ist
if not callable(func):
raise ValueError(
"registered function '{0}' is not callable".format(func)
)
raise ValueError("registered function '{0}' is not callable".format(func))
if type(delay) != int or delay < 0:
raise ValueError(
"'delay' must be <class 'int'> and greater or equal 0"
)
raise ValueError("'delay' must be <class 'int'> and greater or equal 0")
if edge != BOTH and not self._bitshift:
raise ValueError(
"parameter 'edge' can be used with bit io objects only"
)
raise ValueError("parameter 'edge' can be used with bit io objects only")
if prefire and self._parentdevice._modio._looprunning:
raise RuntimeError(
"prefire can not be used if mainloop is running"
)
raise RuntimeError("prefire can not be used if mainloop is running")
if self not in self._parentdevice._dict_events:
with self._parentdevice._filelock:
self._parentdevice._dict_events[self] = \
[IOEvent(func, edge, as_thread, delay, overwrite, prefire)]
self._parentdevice._dict_events[self] = [
IOEvent(func, edge, as_thread, delay, overwrite, prefire)
]
else:
# Prüfen ob Funktion schon registriert ist
for regfunc in self._parentdevice._dict_events[self]:
@@ -476,10 +561,7 @@ class IOBase(object):
raise RuntimeError(
"io '{0}' with function '{1}' already in list "
"with edge '{2}' - edge '{3}' not allowed anymore"
"".format(
self._name, func,
consttostr(regfunc.edge), consttostr(edge)
)
"".format(self._name, func, consttostr(regfunc.edge), consttostr(edge))
)
else:
raise RuntimeError(
@@ -490,9 +572,7 @@ class IOBase(object):
elif regfunc.edge == edge:
raise RuntimeError(
"io '{0}' with function '{1}' for given edge '{2}' "
"already in list".format(
self._name, func, consttostr(edge)
)
"already in list".format(self._name, func, consttostr(edge))
)
# Eventfunktion einfügen
@@ -548,9 +628,7 @@ class IOBase(object):
if self._bitshift:
# Write single bit to process image
value = \
self._parentdevice._ba_devdata[self._slc_address.start] & \
self._bitshift
value = self._parentdevice._ba_devdata[self._slc_address.start] & self._bitshift
if self._parentdevice._modio._run_on_pi:
# IOCTL auf dem RevPi
with self._parentdevice._modio._myfh_lck:
@@ -559,8 +637,7 @@ class IOBase(object):
ioctl(
self._parentdevice._modio._myfh,
19216,
self.__bit_ioctl_on if value
else self.__bit_ioctl_off
self.__bit_ioctl_on if value else self.__bit_ioctl_off,
)
except Exception as e:
self._parentdevice._modio._gotioerror("ioset", e)
@@ -572,12 +649,10 @@ class IOBase(object):
try:
self._parentdevice._modio._myfh.ioctl(
19216,
self.__bit_ioctl_on if value
else self.__bit_ioctl_off
self.__bit_ioctl_on if value else self.__bit_ioctl_off,
)
except Exception as e:
self._parentdevice._modio._gotioerror(
"net_ioset", e)
self._parentdevice._modio._gotioerror("net_ioset", e)
return False
else:
@@ -586,8 +661,7 @@ class IOBase(object):
# Set value durchführen (Funktion K+16)
self._parentdevice._modio._simulate_ioctl(
19216,
self.__bit_ioctl_on if value
else self.__bit_ioctl_off
self.__bit_ioctl_on if value else self.__bit_ioctl_off,
)
except Exception as e:
self._parentdevice._modio._gotioerror("file_ioset", e)
@@ -598,9 +672,7 @@ class IOBase(object):
value = bytes(self._parentdevice._ba_devdata[self._slc_address])
with self._parentdevice._modio._myfh_lck:
try:
self._parentdevice._modio._myfh.seek(
self._get_address()
)
self._parentdevice._modio._myfh.seek(self._get_address())
self._parentdevice._modio._myfh.write(value)
if self._parentdevice._modio._buffedwrite:
self._parentdevice._modio._myfh.flush()
@@ -625,15 +697,11 @@ class IOBase(object):
:return: IO-Wert als <class 'bytes'> oder <class 'bool'>
"""
if self._bitshift:
return bool(
self._parentdevice._ba_devdata[self._slc_address.start]
& self._bitshift
)
return bool(self._parentdevice._ba_devdata[self._slc_address.start] & self._bitshift)
else:
return bytes(self._parentdevice._ba_devdata[self._slc_address])
def reg_event(
self, func, delay=0, edge=BOTH, as_thread=False, prefire=False):
def reg_event(self, func, delay=0, edge=BOTH, as_thread=False, prefire=False):
"""
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)
def reg_timerevent(
self, func, delay, edge=BOTH, as_thread=False, prefire=False):
def reg_timerevent(self, func, delay, edge=BOTH, as_thread=False, prefire=False):
"""
Registriert fuer IO einen Timer, welcher nach delay func ausfuehrt.
@@ -685,20 +752,13 @@ class IOBase(object):
if self._iotype == INP:
if self._parentdevice._modio._simulator:
raise RuntimeError(
"can not write to output '{0}' in simulator mode"
"".format(self._name)
"can not write to output '{0}' in simulator mode".format(self._name)
)
else:
raise RuntimeError(
"can not write to input '{0}'".format(self._name)
)
raise RuntimeError("can not write to input '{0}'".format(self._name))
elif self._iotype == MEM:
raise RuntimeError(
"can not write to memory '{0}'".format(self._name)
)
raise RuntimeError(
"the io object '{0}' is read only".format(self._name)
)
raise RuntimeError("can not write to memory '{0}'".format(self._name))
raise RuntimeError("the io object '{0}' is read only".format(self._name))
if self._bitshift:
# Versuchen egal welchen Typ in Bool zu konvertieren
@@ -722,8 +782,7 @@ class IOBase(object):
int_byte -= self._bitshift
# Zurückschreiben wenn verändert
self._parentdevice._ba_devdata[self._slc_address.start] = \
int_byte
self._parentdevice._ba_devdata[self._slc_address.start] = int_byte
self._parentdevice._filelock.release()
@@ -738,9 +797,7 @@ class IOBase(object):
if self._length != len(value):
raise ValueError(
"'{0}' requires a <class 'bytes'> object of "
"length {1}, but {2} was given".format(
self._name, self._length, len(value)
)
"length {1}, but {2} was given".format(self._name, self._length, len(value))
)
if self._parentdevice._shared_procimg:
@@ -764,8 +821,7 @@ class IOBase(object):
else:
newlist = []
for regfunc in self._parentdevice._dict_events[self]:
if regfunc.func != func or edge is not None \
and regfunc.edge != edge:
if regfunc.func != func or edge is not None and regfunc.edge != edge:
newlist.append(regfunc)
# Wenn Funktionen übrig bleiben, diese übernehmen
@@ -832,17 +888,11 @@ class IOBase(object):
"revpimodio2.FALLING or revpimodio2.BOTH"
)
if not (exitevent is None or type(exitevent) == Event):
raise TypeError(
"parameter 'exitevent' must be <class 'threading.Event'>"
)
raise TypeError("parameter 'exitevent' must be <class 'threading.Event'>")
if type(timeout) != int or timeout < 0:
raise ValueError(
"parameter 'timeout' must be <class 'int'> and greater than 0"
)
raise ValueError("parameter 'timeout' must be <class 'int'> and greater than 0")
if edge != BOTH and not self._bitshift:
raise ValueError(
"parameter 'edge' can be used with bit Inputs only"
)
raise ValueError("parameter 'edge' can be used with bit Inputs only")
# Abbruchwert prüfen
if okvalue == self.value:
@@ -858,23 +908,27 @@ class IOBase(object):
exitevent = Event()
flt_timecount = 0 if bool_timecount else -1
while not self._parentdevice._modio._waitexit.is_set() \
and not exitevent.is_set() \
and flt_timecount < timeout:
while (
not self._parentdevice._modio._waitexit.is_set()
and not exitevent.is_set()
and flt_timecount < timeout
):
if self._parentdevice._modio._imgwriter.newdata.wait(2.5):
self._parentdevice._modio._imgwriter.newdata.clear()
if val_start != self.value:
if edge == BOTH \
or edge == RISING and not val_start \
or edge == FALLING and val_start:
if (
edge == BOTH
or edge == RISING
and not val_start
or edge == FALLING
and val_start
):
return 0
else:
val_start = not val_start
if bool_timecount:
flt_timecount += \
self._parentdevice._modio._imgwriter._refresh
flt_timecount += self._parentdevice._modio._imgwriter._refresh
elif bool_timecount:
flt_timecount += 2.5
@@ -922,7 +976,7 @@ class IntIO(IOBase):
return int.from_bytes(
self._parentdevice._ba_devdata[self._slc_address],
byteorder=self._byteorder,
signed=self._signed
signed=self._signed,
)
def __call__(self, value=None):
@@ -931,16 +985,18 @@ class IntIO(IOBase):
return int.from_bytes(
self._parentdevice._ba_devdata[self._slc_address],
byteorder=self._byteorder,
signed=self._signed
signed=self._signed,
)
else:
# Inline from set_intvalue()
if type(value) == int:
self.set_value(value.to_bytes(
self._length,
byteorder=self._byteorder,
signed=self._signed
))
self.set_value(
value.to_bytes(
self._length,
byteorder=self._byteorder,
signed=self._signed,
)
)
else:
raise TypeError(
"'{0}' need a <class 'int'> value, but {1} was given"
@@ -983,9 +1039,7 @@ class IntIO(IOBase):
:return: <class 'int'> Defaultvalue
"""
return int.from_bytes(
self._defaultvalue, byteorder=self._byteorder, signed=self._signed
)
return int.from_bytes(self._defaultvalue, byteorder=self._byteorder, signed=self._signed)
def get_intvalue(self) -> int:
"""
@@ -996,7 +1050,7 @@ class IntIO(IOBase):
return int.from_bytes(
self._parentdevice._ba_devdata[self._slc_address],
byteorder=self._byteorder,
signed=self._signed
signed=self._signed,
)
def set_intvalue(self, value: int) -> None:
@@ -1006,11 +1060,13 @@ class IntIO(IOBase):
:param value: <class 'int'> Wert
"""
if type(value) == int:
self.set_value(value.to_bytes(
self._length,
byteorder=self._byteorder,
signed=self._signed
))
self.set_value(
value.to_bytes(
self._length,
byteorder=self._byteorder,
signed=self._signed,
)
)
else:
raise TypeError(
"'{0}' need a <class 'int'> value, but {1} was given"
@@ -1028,9 +1084,7 @@ class IntIOCounter(IntIO):
__slots__ = ("__ioctl_arg",)
def __init__(
self, counter_id,
parentdevice, valuelist, iotype, byteorder, signed):
def __init__(self, counter_id, parentdevice, valuelist, iotype, byteorder, signed):
"""
Instantiierung der IntIOCounter-Klasse.
@@ -1044,10 +1098,11 @@ class IntIOCounter(IntIO):
# Deviceposition + leer + Counter_ID
# ID-Bits: 7|6|5|4|3|2|1|0|15|14|13|12|11|10|9|8
self.__ioctl_arg = \
parentdevice._position.to_bytes(1, "little") \
+ b'\x00' \
self.__ioctl_arg = (
parentdevice._position.to_bytes(1, "little")
+ b"\x00"
+ (1 << counter_id).to_bytes(2, "little")
)
"""
IOCTL fuellt dieses struct, welches durch padding im Speicher nach
@@ -1067,23 +1122,16 @@ class IntIOCounter(IntIO):
def reset(self) -> None:
"""Setzt den Counter des Inputs zurueck."""
if self._parentdevice._modio._monitoring:
raise RuntimeError(
"can not reset counter, while system is in monitoring mode"
)
raise RuntimeError("can not reset counter, while system is in monitoring mode")
if self._parentdevice._modio._simulator:
raise RuntimeError(
"can not reset counter, while system is in simulator mode"
)
raise RuntimeError("can not reset counter, while system is in simulator mode")
if self._parentdevice._modio._run_on_pi:
# IOCTL auf dem RevPi
with self._parentdevice._modio._myfh_lck:
try:
# Counter reset durchführen (Funktion K+20)
ioctl(
self._parentdevice._modio._myfh,
19220, self.__ioctl_arg
)
ioctl(self._parentdevice._modio._myfh, 19220, self.__ioctl_arg)
except Exception as e:
self._parentdevice._modio._gotioerror("iorst", e)
@@ -1091,9 +1139,7 @@ class IntIOCounter(IntIO):
# IOCTL über Netzwerk
with self._parentdevice._modio._myfh_lck:
try:
self._parentdevice._modio._myfh.ioctl(
19220, self.__ioctl_arg
)
self._parentdevice._modio._myfh.ioctl(19220, self.__ioctl_arg)
except Exception as e:
self._parentdevice._modio._gotioerror("net_iorst", e)
@@ -1101,9 +1147,7 @@ class IntIOCounter(IntIO):
# IOCTL in Datei simulieren
try:
# Set value durchführen (Funktion K+20)
self._parentdevice._modio._simulate_ioctl(
19220, self.__ioctl_arg
)
self._parentdevice._modio._simulate_ioctl(19220, self.__ioctl_arg)
except Exception as 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>`_
"""
# StructIO erzeugen
io_new = StructIO(
self,
name,
frm,
**kwargs
)
io_new = StructIO(self, name, frm, **kwargs)
# StructIO in IO-Liste einfügen
self._parentdevice._modio.io._private_register_new_io_object(io_new)
@@ -1170,10 +1209,129 @@ class IntIOReplaceable(IntIO):
reg_event,
kwargs.get("delay", 0),
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):
"""
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.
"""
__slots__ = "__frm", "_parentio_address", "_parentio_defaultvalue", \
"_parentio_length", "_parentio_name", "_wordorder"
__slots__ = (
"__frm",
"_parentio_address",
"_parentio_defaultvalue",
"_parentio_length",
"_parentio_name",
"_wordorder",
)
def __init__(self, parentio, name: str, frm: str, **kwargs):
"""
@@ -1215,15 +1379,12 @@ class StructIO(IOBase):
if frm == "?":
if self._wordorder:
raise ValueError(
"you can not use wordorder for bit based ios"
)
raise ValueError("you can not use wordorder for bit based ios")
bitaddress = kwargs.get("bit", 0)
max_bits = parentio._length * 8
if not (0 <= bitaddress < max_bits):
raise ValueError(
"bitaddress must be a value between 0 and {0}"
"".format(max_bits - 1)
"bitaddress must be a value between 0 and {0}".format(max_bits - 1)
)
bitlength = 1
@@ -1246,8 +1407,7 @@ class StructIO(IOBase):
raise ValueError("wordorder must be 'little' or 'big'")
if byte_length % 2 != 0:
raise ValueError(
"the byte length of new io must must be even to "
"use wordorder"
"the byte length of new io must must be even to use wordorder"
)
# [name,default,anzbits,adressbyte,export,adressid,bmk,bitaddress]
@@ -1260,7 +1420,7 @@ class StructIO(IOBase):
False,
str(parentio._slc_address.start).rjust(4, "0"),
kwargs.get("bmk", ""),
bitaddress
bitaddress,
]
else:
raise ValueError(
@@ -1270,11 +1430,7 @@ class StructIO(IOBase):
# Basisklasse instantiieren
super().__init__(
parentio._parentdevice,
valuelist,
parentio._iotype,
byteorder,
frm == frm.lower()
parentio._parentdevice, valuelist, parentio._iotype, byteorder, frm == frm.lower()
)
self.__frm = bofrm + frm
if "export" in kwargs:
@@ -1286,13 +1442,11 @@ class StructIO(IOBase):
self._export = parentio._export
# Platz für neuen IO prüfen
if not (self._slc_address.start >=
parentio._parentdevice._dict_slc[parentio._iotype].start and
self._slc_address.stop <=
parentio._parentdevice._dict_slc[parentio._iotype].stop):
raise BufferError(
"registered value does not fit process image scope"
)
if not (
self._slc_address.start >= parentio._parentdevice._dict_slc[parentio._iotype].start
and self._slc_address.stop <= parentio._parentdevice._dict_slc[parentio._iotype].stop
):
raise BufferError("registered value does not fit process image scope")
def __call__(self, value=None):
if value is None:
@@ -1310,9 +1464,7 @@ class StructIO(IOBase):
if self._bitshift:
self.set_value(value)
elif self._wordorder == "little" and self._length > 2:
self.set_value(
self._swap_word_order(struct.pack(self.__frm, value))
)
self.set_value(self._swap_word_order(struct.pack(self.__frm, value)))
else:
self.set_value(struct.pack(self.__frm, value))
@@ -1343,8 +1495,10 @@ class StructIO(IOBase):
array_length = len(bytes_to_swap)
swap_array = bytearray(bytes_to_swap)
for i in range(0, array_length // 2, 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 - 2 : array_length - i], swap_array[i : i + 2] = (
swap_array[i : i + 2],
swap_array[-i - 2 : array_length - i],
)
return bytes(swap_array)
def get_structdefaultvalue(self):
@@ -1394,9 +1548,7 @@ class StructIO(IOBase):
if self._bitshift:
self.set_value(value)
elif self._wordorder == "little" and self._length > 2:
self.set_value(
self._swap_word_order(struct.pack(self.__frm, value))
)
self.set_value(self._swap_word_order(struct.pack(self.__frm, value)))
else:
self.set_value(struct.pack(self.__frm, value))
@@ -1422,7 +1574,7 @@ class MemIO(IOBase):
if self._bitlength > 64:
# STRING
try:
val = val.strip(b'\x00').decode()
val = val.strip(b"\x00").decode()
except Exception:
pass
return val

View File

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

View File

@@ -18,22 +18,22 @@ from .modio import DevSelect, RevPiModIO as _RevPiModIO
from .pictory import DeviceType
# 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
_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
_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
_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'
_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"
# ReplaceIO Konfiguration laden
_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'
_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"
# 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 = b'\x01'
HEADER_STOP = b'\x17'
HEADER_START = b"\x01"
HEADER_STOP = b"\x17"
class AclException(Exception):
@@ -57,12 +57,28 @@ class NetFH(Thread):
so gesteuert werden.
"""
__slots__ = "__buff_size", "__buff_block", "__buff_recv", \
"__by_buff", "__check_replace_ios", "__config_changed", \
"__int_buff", "__dictdirty", "__flusherr", "__replace_ios_h", \
"__pictory_h", "__position", "__sockerr", "__sockend", \
"__socklock", "__timeout", "__waitsync", "_address", \
"_serversock", "daemon"
__slots__ = (
"__buff_size",
"__buff_block",
"__buff_recv",
"__by_buff",
"__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):
"""
@@ -83,8 +99,8 @@ class NetFH(Thread):
self.__config_changed = False
self.__int_buff = 0
self.__dictdirty = {}
self.__replace_ios_h = b''
self.__pictory_h = b''
self.__replace_ios_h = b""
self.__pictory_h = b""
self.__sockerr = Event()
self.__sockend = Event()
self.__socklock = Lock()
@@ -95,9 +111,7 @@ class NetFH(Thread):
# Parameterprüfung
if not isinstance(address, tuple):
raise TypeError(
"parameter address must be <class 'tuple'> ('IP', PORT)"
)
raise TypeError("parameter address must be <class 'tuple'> ('IP', PORT)")
if not isinstance(timeout, int):
raise TypeError("parameter timeout must be <class 'int'>")
@@ -125,7 +139,7 @@ class NetFH(Thread):
:param bytecode: Antwort, die geprueft werden solll
"""
if bytecode == b'\x18':
if bytecode == b"\x18":
# Alles beenden, wenn nicht erlaubt
self.__sockend.set()
self.__sockerr.set()
@@ -174,7 +188,7 @@ class NetFH(Thread):
buff_recv = bytearray(recv_len)
while recv_len > 0:
block = so.recv(recv_len)
if block == b'':
if block == b"":
raise OSError("lost connection on hash receive")
buff_recv += block
recv_len -= len(block)
@@ -183,18 +197,19 @@ class NetFH(Thread):
if self.__pictory_h and buff_recv[:16] != self.__pictory_h:
self.__config_changed = True
self.close()
raise ConfigChanged(
"configuration on revolution pi was changed")
raise ConfigChanged("configuration on revolution pi was changed")
else:
self.__pictory_h = buff_recv[:16]
# Änderung an replace_ios prüfen
if self.__check_replace_ios and self.__replace_ios_h \
and buff_recv[16:] != self.__replace_ios_h:
if (
self.__check_replace_ios
and self.__replace_ios_h
and buff_recv[16:] != self.__replace_ios_h
):
self.__config_changed = True
self.close()
raise ConfigChanged(
"configuration on revolution pi was changed")
raise ConfigChanged("configuration on revolution pi was changed")
else:
self.__replace_ios_h = buff_recv[16:]
except ConfigChanged:
@@ -295,11 +310,18 @@ class NetFH(Thread):
else:
# Nur bestimmte Dirtybytes löschen
# b CM ii xx c0000000 b = 16
buff = self._direct_sr(pack(
"=c2sH2xc7xc",
HEADER_START, b'EY', position, b'\xfe', HEADER_STOP
), 1)
if buff != b'\x1e':
buff = self._direct_sr(
pack(
"=c2sH2xc7xc",
HEADER_START,
b"EY",
position,
b"\xfe",
HEADER_STOP,
),
1,
)
if buff != b"\x1e":
# ACL prüfen und ggf Fehler werfen
self.__check_acl(buff)
@@ -343,12 +365,18 @@ class NetFH(Thread):
try:
# b CM ii ii 00000000 b = 16
buff = self._direct_sr(pack(
"=c2sHH8xc",
HEADER_START,
b'FD', self.__int_buff, len(self.__by_buff),
HEADER_STOP
) + self.__by_buff, 1)
buff = self._direct_sr(
pack(
"=c2sHH8xc",
HEADER_START,
b"FD",
self.__int_buff,
len(self.__by_buff),
HEADER_STOP,
)
+ self.__by_buff,
1,
)
except Exception:
raise
finally:
@@ -356,7 +384,7 @@ class NetFH(Thread):
self.__int_buff = 0
self.__by_buff.clear()
if buff != b'\x1e':
if buff != b"\x1e":
# ACL prüfen und ggf Fehler werfen
self.__check_acl(buff)
@@ -403,7 +431,7 @@ class NetFH(Thread):
"""
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.
@@ -419,11 +447,10 @@ class NetFH(Thread):
raise TypeError("arg must be <class 'bytes'>")
# b CM xx ii iiii0000 b = 16
buff = self._direct_sr(pack(
"=c2s2xHI4xc",
HEADER_START, b'IC', len(arg), request, HEADER_STOP
) + arg, 1)
if buff != b'\x1e':
buff = self._direct_sr(
pack("=c2s2xHI4xc", HEADER_START, b"IC", len(arg), request, HEADER_STOP) + arg, 1
)
if buff != b"\x1e":
# ACL prüfen und ggf Fehler werfen
self.__check_acl(buff)
@@ -443,10 +470,9 @@ class NetFH(Thread):
raise ValueError("read of closed file")
# b CM ii ii 00000000 b = 16
buff = self._direct_sr(pack(
"=c2sHH8xc",
HEADER_START, b'DA', self.__position, length, HEADER_STOP
), length)
buff = self._direct_sr(
pack("=c2sHH8xc", HEADER_START, b"DA", self.__position, length, HEADER_STOP), length
)
self.__position += length
return buff
@@ -466,10 +492,9 @@ class NetFH(Thread):
length = len(buffer)
# b CM ii ii 00000000 b = 16
buff = self._direct_sr(pack(
"=c2sHH8xc",
HEADER_START, b'DA', self.__position, length, HEADER_STOP
), length)
buff = self._direct_sr(
pack("=c2sHH8xc", HEADER_START, b"DA", self.__position, length, HEADER_STOP), length
)
buffer[:] = buff
return len(buffer)
@@ -484,13 +509,11 @@ class NetFH(Thread):
raise ValueError("read of closed file")
if self.__pictory_h == HASH_FAIL:
raise RuntimeError(
"could not read/parse piCtory configuration over network"
)
raise RuntimeError("could not read/parse piCtory configuration over network")
buff = self._direct_sr(_syspictory, 4)
recv_length, = unpack("=I", buff)
return self._direct_sr(b'', recv_length)
(recv_length,) = unpack("=I", buff)
return self._direct_sr(b"", recv_length)
def readreplaceio(self) -> bytes:
"""
@@ -502,27 +525,21 @@ class NetFH(Thread):
raise ValueError("read of closed file")
if self.__replace_ios_h == HASH_FAIL:
raise RuntimeError(
"replace_io_file: could not read/parse over network"
)
raise RuntimeError("replace_io_file: could not read/parse over network")
buff = self._direct_sr(_sysreplaceio, 4)
recv_length, = unpack("=I", buff)
return self._direct_sr(b'', recv_length)
(recv_length,) = unpack("=I", buff)
return self._direct_sr(b"", recv_length)
def run(self) -> None:
"""Handler fuer Synchronisierung."""
state_reconnect = False
while not self.__sockend.is_set():
# Bei Fehlermeldung neu verbinden
if self.__sockerr.is_set():
if not state_reconnect:
state_reconnect = True
warnings.warn(
"got a network error and try to reconnect",
RuntimeWarning
)
warnings.warn("got a network error and try to reconnect", RuntimeWarning)
self._connect()
if self.__sockerr.is_set():
# Verhindert beim Scheitern 100% CPU last
@@ -530,10 +547,7 @@ class NetFH(Thread):
continue
else:
state_reconnect = False
warnings.warn(
"successfully reconnected after network error",
RuntimeWarning
)
warnings.warn("successfully reconnected after network error", RuntimeWarning)
# Kein Fehler aufgetreten, sync durchführen wenn socket frei
if self.__socklock.acquire(blocking=False):
@@ -543,9 +557,7 @@ class NetFH(Thread):
self.__buff_recv.clear()
recv_lenght = 2
while recv_lenght > 0:
count = self._serversock.recv_into(
self.__buff_block, recv_lenght
)
count = self._serversock.recv_into(self.__buff_block, recv_lenght)
if count == 0:
raise IOError("lost network connection on sync")
self.__buff_recv += self.__buff_block[:count]
@@ -554,11 +566,8 @@ class NetFH(Thread):
except IOError:
self.__sockerr.set()
else:
if self.__buff_recv != b'\x06\x16':
warnings.warn(
"data error on network sync",
RuntimeWarning
)
if self.__buff_recv != b"\x06\x16":
warnings.warn("data error on network sync", RuntimeWarning)
self.__sockerr.set()
continue
finally:
@@ -596,12 +605,13 @@ class NetFH(Thread):
try:
# b CM ii ii 00000000 b = 16
buff = self._direct_sr(pack(
"=c2sHH8xc",
HEADER_START, b'EY', position, len(dirtybytes), HEADER_STOP
) + dirtybytes, 1)
buff = self._direct_sr(
pack("=c2sHH8xc", HEADER_START, b"EY", position, len(dirtybytes), HEADER_STOP)
+ dirtybytes,
1,
)
if buff != b'\x1e':
if buff != b"\x1e":
# ACL prüfen und ggf Fehler werfen
self.__check_acl(buff)
@@ -627,11 +637,8 @@ class NetFH(Thread):
try:
# b CM ii xx 00000000 b = 16
buff = self._direct_sr(pack(
"=c2sH10xc",
HEADER_START, b'CF', value, HEADER_STOP
), 1)
if buff != b'\x1e':
buff = self._direct_sr(pack("=c2sH10xc", HEADER_START, b"CF", value, HEADER_STOP), 1)
if buff != b"\x1e":
raise IOError("set timeout error on network")
except Exception:
self.__sockerr.set()
@@ -666,10 +673,11 @@ class NetFH(Thread):
self.__int_buff += 1
# Datenblock mit Position und Länge in Puffer ablegen
self.__by_buff += \
self.__position.to_bytes(length=2, byteorder="little") \
+ len(bytebuff).to_bytes(length=2, byteorder="little") \
self.__by_buff += (
self.__position.to_bytes(length=2, byteorder="little")
+ len(bytebuff).to_bytes(length=2, byteorder="little")
+ bytebuff
)
# TODO: Bufferlänge und dann flushen?
@@ -697,9 +705,16 @@ class RevPiNetIO(_RevPiModIO):
__slots__ = "_address"
def __init__(
self, address, autorefresh=False, monitoring=False,
syncoutputs=True, simulator=False, debug=True,
replace_io_file=None, shared_procimg=False, direct_output=False):
self,
address,
autorefresh=False,
monitoring=False,
syncoutputs=True,
simulator=False,
debug=True,
replace_io_file=None,
shared_procimg=False,
):
"""
Instantiiert die Grundfunktionen.
@@ -712,30 +727,21 @@ class RevPiNetIO(_RevPiModIO):
:param replace_io_file: Replace IO Konfiguration aus Datei laden
:param shared_procimg: Share process image with other processes, this
could be insecure for automation
:param direct_output: Deprecated, use shared_procimg
"""
check_ip = compile(
r"^(25[0-5]|(2[0-4]|[01]?\d|)\d)"
r"(\.(25[0-5]|(2[0-4]|[01]?\d|)\d)){3}$"
)
check_ip = compile(r"^(25[0-5]|(2[0-4]|[01]?\d|)\d)(\.(25[0-5]|(2[0-4]|[01]?\d|)\d)){3}$")
# Adresse verarbeiten
if isinstance(address, str):
self._address = (address, 55234)
elif isinstance(address, tuple):
if len(address) == 2 \
and isinstance(address[0], str) \
and isinstance(address[1], int):
if len(address) == 2 and isinstance(address[0], str) and isinstance(address[1], int):
# Werte prüfen
if not 0 < address[1] <= 65535:
raise ValueError("port number out of range 1 - 65535")
self._address = address
else:
raise TypeError(
"address tuple must be (<class 'str'>, <class 'int'>)"
)
raise TypeError("address tuple must be (<class 'str'>, <class 'int'>)")
else:
raise TypeError(
"parameter address must be <class 'str'> or <class 'tuple'> "
@@ -749,8 +755,7 @@ class RevPiNetIO(_RevPiModIO):
self._address = (ipv4, self._address[1])
except Exception:
raise ValueError(
"can not resolve ip address for hostname '{0}'"
"".format(self._address[0])
"can not resolve ip address for hostname '{0}'".format(self._address[0])
)
# Vererben
@@ -764,7 +769,6 @@ class RevPiNetIO(_RevPiModIO):
debug=debug,
replace_io_file=replace_io_file,
shared_procimg=shared_procimg,
direct_output=direct_output,
)
self._set_device_based_cycle_time = False
@@ -801,10 +805,7 @@ class RevPiNetIO(_RevPiModIO):
try:
cp.read_string(byte_buff.decode("utf-8"))
except Exception as e:
raise RuntimeError(
"replace_io_file: could not read/parse network data | {0}"
"".format(e)
)
raise RuntimeError("replace_io_file: could not read/parse network data | {0}".format(e))
return cp
def disconnect(self) -> None:
@@ -862,16 +863,12 @@ class RevPiNetIO(_RevPiModIO):
:param device: nur auf einzelnes Device anwenden, sonst auf Alle
"""
if self.monitoring:
raise RuntimeError(
"can not send default values, while system is in "
"monitoring mode"
)
raise RuntimeError("can not send default values, while system is in monitoring mode")
if device is None:
self._myfh.clear_dirtybytes()
else:
dev = device if isinstance(device, Device) \
else self.device.__getitem__(device)
dev = device if isinstance(device, Device) else self.device.__getitem__(device)
mylist = [dev]
for dev in mylist:
@@ -887,16 +884,12 @@ class RevPiNetIO(_RevPiModIO):
:param device: nur auf einzelnes Device anwenden, sonst auf Alle
"""
if self.monitoring:
raise RuntimeError(
"can not send default values, while system is in "
"monitoring mode"
)
raise RuntimeError("can not send default values, while system is in monitoring mode")
if device is None:
mylist = self.device
else:
dev = device if isinstance(device, Device) \
else self.device.__getitem__(device)
dev = device if isinstance(device, Device) else self.device.__getitem__(device)
mylist = [dev]
for dev in mylist:
@@ -921,13 +914,10 @@ class RevPiNetIO(_RevPiModIO):
int_byte += 1 if bitio._defaultvalue else 0
# Errechneten Int-Wert in ein Byte umwandeln
dirtybytes += \
int_byte.to_bytes(length=1, byteorder="little")
dirtybytes += int_byte.to_bytes(length=1, byteorder="little")
# Dirtybytes an PLC Server senden
self._myfh.set_dirtybytes(
dev._offset + dev._slc_out.start, dirtybytes
)
self._myfh.set_dirtybytes(dev._offset + dev._slc_out.start, dirtybytes)
config_changed = property(get_config_changed)
reconnecting = property(get_reconnecting)
@@ -946,9 +936,17 @@ class RevPiNetIOSelected(RevPiNetIO):
__slots__ = ()
def __init__(
self, address, deviceselection, autorefresh=False,
monitoring=False, syncoutputs=True, simulator=False, debug=True,
replace_io_file=None, shared_procimg=False, direct_output=False):
self,
address,
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.
@@ -961,8 +959,14 @@ class RevPiNetIOSelected(RevPiNetIO):
:ref: :func:`RevPiNetIO.__init__()`
"""
super().__init__(
address, autorefresh, monitoring, syncoutputs, simulator, debug,
replace_io_file, shared_procimg, direct_output
address,
autorefresh,
monitoring,
syncoutputs,
simulator,
debug,
replace_io_file,
shared_procimg,
)
if type(deviceselection) is not DevSelect:
@@ -981,24 +985,19 @@ class RevPiNetIOSelected(RevPiNetIO):
if len(self.device) == 0:
if self._devselect.type:
raise DeviceNotFoundError(
"could not find ANY given {0} devices in config"
"".format(self._devselect.type)
"could not find ANY given {0} devices in config".format(self._devselect.type)
)
else:
raise DeviceNotFoundError(
"could not find ANY given devices in config"
)
elif not self._devselect.other_device_key \
and len(self.device) != len(self._devselect.values):
raise DeviceNotFoundError("could not find ANY given devices in config")
elif not self._devselect.other_device_key and len(self.device) != len(
self._devselect.values
):
if self._devselect.type:
raise DeviceNotFoundError(
"could not find ALL given {0} devices in config"
"".format(self._devselect.type)
"could not find ALL given {0} devices in config".format(self._devselect.type)
)
else:
raise DeviceNotFoundError(
"could not find ALL given devices in config"
)
raise DeviceNotFoundError("could not find ALL given devices in config")
class RevPiNetIODriver(RevPiNetIOSelected):
@@ -1014,9 +1013,15 @@ class RevPiNetIODriver(RevPiNetIOSelected):
__slots__ = ()
def __init__(
self, address, virtdev, autorefresh=False,
syncoutputs=True, debug=True, replace_io_file=None,
shared_procimg=False, direct_output=False):
self,
address,
virtdev,
autorefresh=False,
syncoutputs=True,
debug=True,
replace_io_file=None,
shared_procimg=False,
):
"""
Instantiiert die Grundfunktionen.
@@ -1032,13 +1037,19 @@ class RevPiNetIODriver(RevPiNetIOSelected):
virtdev = (virtdev,)
dev_select = DevSelect(DeviceType.VIRTUAL, "", virtdev)
super().__init__(
address, dev_select, autorefresh, False, syncoutputs, True, debug,
replace_io_file, shared_procimg, direct_output
address,
dev_select,
autorefresh,
False,
syncoutputs,
True,
debug,
replace_io_file,
shared_procimg,
)
def run_net_plc(
address, func, cycletime=50, replace_io_file=None, debug=True):
def run_net_plc(address, func, cycletime=50, replace_io_file=None, debug=True):
"""
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
# - RevPiGateCANopen_20161102_1_0.rap
class ProductType:
CON_BT = 111
CON_CAN = 109
CON_MBUS = 110
GATEWAY_CAN_OPEN = 71
GATEWAY_CCLINK = 72
GATEWAY_DEV_NET = 73
@@ -38,6 +43,7 @@ class ProductType:
DO = 98
AIO = 103
MIO = 118
RO = 137
REVPI_CORE = 95
REVPI_COMPACT = 104
@@ -45,9 +51,22 @@ class ProductType:
REVPI_FLAT = 135
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:
"""Module key "type" in piCtory file."""
IGNORED = ""
BASE = "BASE" # Core devices
EDGE = "EDGE" # Gateways
@@ -58,6 +77,7 @@ class DeviceType:
class AIO:
"""Memory value mappings for RevPi AIO 1.0 (RevPiAIO_20170301_1_0.rap)."""
OUT_RANGE_OFF = 0 # Off
OUT_RANGE_0_5V = 1 # 0 - 5V
OUT_RANGE_0_10V = 2 # 0 - 10V
@@ -131,6 +151,7 @@ class AIO:
class DI:
"""Memory value mappings for RevPi DI 1.0 (RevPiDI_20160818_1_0.rap)."""
IN_MODE_DIRECT = 0 # Direct
IN_MODE_COUNT_RISING = 1 # Counter, rising edge
IN_MODE_COUNT_FALLING = 2 # Counter, falling edge
@@ -144,6 +165,7 @@ class DI:
class DO:
"""Memory value mappings for RevPi DO 1.0 (RevPiDO_20160818_1_0.rap)."""
OUT_PWM_FREQ_40HZ = 1 # 40Hz 1%
OUT_PWM_FREQ_80HZ = 2 # 80Hz 2%
OUT_PWM_FREQ_160HZ = 4 # 160Hz 4%
@@ -153,11 +175,13 @@ class DO:
class DIO(DI, DO):
"""Memory value mappings for RevPi DIO 1.0 (RevPiDIO_20160818_1_0.rap)."""
pass
class MIO:
"""Memory value mappings for RevPi MIO 1.0 (RevPiMIO_20200901_1_0.rap)."""
ENCODER_MODE_DISABLED = 0
ENCODER_MODE_ENABLED = 1
@@ -176,6 +200,7 @@ class MIO:
class COMPACT:
"""Memory value mappings for RevPi Compact 1.0 (RevPiCompact_20171023_1_0.rap)."""
DIN_DEBOUNCE_OFF = 0 # Off
DIN_DEBOUNCE_25US = 1 # 25us
DIN_DEBOUNCE_750US = 2 # 750us
@@ -189,5 +214,6 @@ class COMPACT:
class FLAT:
"""Memory value mappings for RevPi Flat 1.0 (RevPiFlat_20200921_1_0.rap)."""
IN_RANGE_0_10V = 0
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)