init
This commit is contained in:
commit
5242eeabb2
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*.FCStd1
|
||||||
|
|
||||||
|
*.old
|
||||||
|
wifi-credentials
|
675
LICENSE.txt
Normal file
675
LICENSE.txt
Normal file
@ -0,0 +1,675 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
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
|
||||||
|
them 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.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. 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.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey 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;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If 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 convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU 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 that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
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.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
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 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
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
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 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program 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.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program 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, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU 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. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
|
|
10
README.md
Normal file
10
README.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Plant Watering device
|
||||||
|
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
https://github.com/robert-hh/SH1106
|
||||||
|
https://github.com/MikeTeachman/micropython-rotary
|
||||||
|
https://github.com/microhomie/microhomie
|
||||||
|
https://github.com/robert-hh/BME280
|
||||||
|
|
||||||
|
https://www.electronicdesign.com/technologies/analog/article/21796004/use-analog-techniques-to-measure-capacitance-in-capacitive-sensors
|
BIN
cad/T-valve-hose.FCStd
Normal file
BIN
cad/T-valve-hose.FCStd
Normal file
Binary file not shown.
BIN
cad/compartment-wall.FCStd
Normal file
BIN
cad/compartment-wall.FCStd
Normal file
Binary file not shown.
BIN
cad/control-panel-bracket.FCStd
Normal file
BIN
cad/control-panel-bracket.FCStd
Normal file
Binary file not shown.
2
code/boot.py
Normal file
2
code/boot.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# import webrepl
|
||||||
|
# webrepl.start()
|
261
code/lib/bme280.py
Normal file
261
code/lib/bme280.py
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
# Updated 2018 and 2020
|
||||||
|
# This module is based on the below cited resources, which are all
|
||||||
|
# based on the documentation as provided in the Bosch Data Sheet and
|
||||||
|
# the sample implementation provided therein.
|
||||||
|
#
|
||||||
|
# Final Document: BST-BME280-DS002-15
|
||||||
|
#
|
||||||
|
# Authors: Paul Cunnane 2016, Peter Dahlebrg 2016
|
||||||
|
#
|
||||||
|
# This module borrows from the Adafruit BME280 Python library. Original
|
||||||
|
# Copyright notices are reproduced below.
|
||||||
|
#
|
||||||
|
# Those libraries were written for the Raspberry Pi. This modification is
|
||||||
|
# intended for the MicroPython and esp8266 boards.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2014 Adafruit Industries
|
||||||
|
# Author: Tony DiCola
|
||||||
|
#
|
||||||
|
# Based on the BMP280 driver with BME280 changes provided by
|
||||||
|
# David J Taylor, Edinburgh (www.satsignal.eu)
|
||||||
|
#
|
||||||
|
# Based on Adafruit_I2C.py created by Kevin Townsend.
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
#
|
||||||
|
|
||||||
|
import time
|
||||||
|
from ustruct import unpack, unpack_from
|
||||||
|
from array import array
|
||||||
|
|
||||||
|
# BME280 default address.
|
||||||
|
BME280_I2CADDR = 0x76
|
||||||
|
|
||||||
|
# Operating Modes
|
||||||
|
BME280_OSAMPLE_1 = 1
|
||||||
|
BME280_OSAMPLE_2 = 2
|
||||||
|
BME280_OSAMPLE_4 = 3
|
||||||
|
BME280_OSAMPLE_8 = 4
|
||||||
|
BME280_OSAMPLE_16 = 5
|
||||||
|
|
||||||
|
BME280_REGISTER_CONTROL_HUM = 0xF2
|
||||||
|
BME280_REGISTER_STATUS = 0xF3
|
||||||
|
BME280_REGISTER_CONTROL = 0xF4
|
||||||
|
|
||||||
|
MODE_SLEEP = const(0)
|
||||||
|
MODE_FORCED = const(1)
|
||||||
|
MODE_NORMAL = const(3)
|
||||||
|
|
||||||
|
BME280_TIMEOUT = const(100) # about 1 second timeout
|
||||||
|
|
||||||
|
|
||||||
|
class BME280:
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
mode=BME280_OSAMPLE_8,
|
||||||
|
address=BME280_I2CADDR,
|
||||||
|
i2c=None,
|
||||||
|
**kwargs):
|
||||||
|
# Check that mode is valid.
|
||||||
|
if mode not in [BME280_OSAMPLE_1, BME280_OSAMPLE_2, BME280_OSAMPLE_4,
|
||||||
|
BME280_OSAMPLE_8, BME280_OSAMPLE_16]:
|
||||||
|
raise ValueError(
|
||||||
|
'Unexpected mode value {0}. Set mode to one of '
|
||||||
|
'BME280_OSAMPLE_1, BME280_OSAMPLE_2, BME280_OSAMPLE_4,'
|
||||||
|
'BME280_OSAMPLE_8, BME280_OSAMPLE_16'.format(mode))
|
||||||
|
self._mode = mode
|
||||||
|
self.address = address
|
||||||
|
if i2c is None:
|
||||||
|
raise ValueError('An I2C object is required.')
|
||||||
|
self.i2c = i2c
|
||||||
|
self.__sealevel = 101325
|
||||||
|
|
||||||
|
# load calibration data
|
||||||
|
dig_88_a1 = self.i2c.readfrom_mem(self.address, 0x88, 26)
|
||||||
|
dig_e1_e7 = self.i2c.readfrom_mem(self.address, 0xE1, 7)
|
||||||
|
|
||||||
|
self.dig_T1, self.dig_T2, self.dig_T3, self.dig_P1, \
|
||||||
|
self.dig_P2, self.dig_P3, self.dig_P4, self.dig_P5, \
|
||||||
|
self.dig_P6, self.dig_P7, self.dig_P8, self.dig_P9, \
|
||||||
|
_, self.dig_H1 = unpack("<HhhHhhhhhhhhBB", dig_88_a1)
|
||||||
|
|
||||||
|
self.dig_H2, self.dig_H3, self.dig_H4,\
|
||||||
|
self.dig_H5, self.dig_H6 = unpack("<hBbhb", dig_e1_e7)
|
||||||
|
# unfold H4, H5, keeping care of a potential sign
|
||||||
|
self.dig_H4 = (self.dig_H4 * 16) + (self.dig_H5 & 0xF)
|
||||||
|
self.dig_H5 //= 16
|
||||||
|
|
||||||
|
# temporary data holders which stay allocated
|
||||||
|
self._l1_barray = bytearray(1)
|
||||||
|
self._l8_barray = bytearray(8)
|
||||||
|
self._l3_resultarray = array("i", [0, 0, 0])
|
||||||
|
|
||||||
|
self._l1_barray[0] = self._mode << 5 | self._mode << 2 | MODE_SLEEP
|
||||||
|
self.i2c.writeto_mem(self.address, BME280_REGISTER_CONTROL,
|
||||||
|
self._l1_barray)
|
||||||
|
self.t_fine = 0
|
||||||
|
|
||||||
|
self.read_compensated_data()
|
||||||
|
|
||||||
|
def read_raw_data(self, result):
|
||||||
|
""" Reads the raw (uncompensated) data from the sensor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: array of length 3 or alike where the result will be
|
||||||
|
stored, in temperature, pressure, humidity order
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._l1_barray[0] = self._mode
|
||||||
|
self.i2c.writeto_mem(self.address, BME280_REGISTER_CONTROL_HUM,
|
||||||
|
self._l1_barray)
|
||||||
|
self._l1_barray[0] = self._mode << 5 | self._mode << 2 | MODE_FORCED
|
||||||
|
self.i2c.writeto_mem(self.address, BME280_REGISTER_CONTROL,
|
||||||
|
self._l1_barray)
|
||||||
|
|
||||||
|
# Wait for conversion to complete
|
||||||
|
for _ in range(BME280_TIMEOUT):
|
||||||
|
if self.i2c.readfrom_mem(self.address, BME280_REGISTER_STATUS, 1)[0] & 0x08:
|
||||||
|
time.sleep_ms(10) # still busy
|
||||||
|
else:
|
||||||
|
break # Sensor ready
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Sensor BME280 not ready")
|
||||||
|
|
||||||
|
# burst readout from 0xF7 to 0xFE, recommended by datasheet
|
||||||
|
self.i2c.readfrom_mem_into(self.address, 0xF7, self._l8_barray)
|
||||||
|
readout = self._l8_barray
|
||||||
|
# pressure(0xF7): ((msb << 16) | (lsb << 8) | xlsb) >> 4
|
||||||
|
raw_press = ((readout[0] << 16) | (readout[1] << 8) | readout[2]) >> 4
|
||||||
|
# temperature(0xFA): ((msb << 16) | (lsb << 8) | xlsb) >> 4
|
||||||
|
raw_temp = ((readout[3] << 16) | (readout[4] << 8) | readout[5]) >> 4
|
||||||
|
# humidity(0xFD): (msb << 8) | lsb
|
||||||
|
raw_hum = (readout[6] << 8) | readout[7]
|
||||||
|
|
||||||
|
result[0] = raw_temp
|
||||||
|
result[1] = raw_press
|
||||||
|
result[2] = raw_hum
|
||||||
|
|
||||||
|
def read_compensated_data(self, result=None):
|
||||||
|
""" Reads the data from the sensor and returns the compensated data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: array of length 3 or alike where the result will be
|
||||||
|
stored, in temperature, pressure, humidity order. You may use
|
||||||
|
this to read out the sensor without allocating heap memory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
array with temperature, pressure, humidity. Will be the one
|
||||||
|
from the result parameter if not None
|
||||||
|
"""
|
||||||
|
self.read_raw_data(self._l3_resultarray)
|
||||||
|
raw_temp, raw_press, raw_hum = self._l3_resultarray
|
||||||
|
# temperature
|
||||||
|
var1 = (raw_temp/16384.0 - self.dig_T1/1024.0) * self.dig_T2
|
||||||
|
var2 = raw_temp/131072.0 - self.dig_T1/8192.0
|
||||||
|
var2 = var2 * var2 * self.dig_T3
|
||||||
|
self.t_fine = int(var1 + var2)
|
||||||
|
temp = (var1 + var2) / 5120.0
|
||||||
|
temp = max(-40, min(85, temp))
|
||||||
|
|
||||||
|
# pressure
|
||||||
|
var1 = (self.t_fine/2.0) - 64000.0
|
||||||
|
var2 = var1 * var1 * self.dig_P6 / 32768.0 + var1 * self.dig_P5 * 2.0
|
||||||
|
var2 = (var2 / 4.0) + (self.dig_P4 * 65536.0)
|
||||||
|
var1 = (self.dig_P3 * var1 * var1 / 524288.0 +
|
||||||
|
self.dig_P2 * var1) / 524288.0
|
||||||
|
var1 = (1.0 + var1 / 32768.0) * self.dig_P1
|
||||||
|
if (var1 == 0.0):
|
||||||
|
pressure = 30000 # avoid exception caused by division by zero
|
||||||
|
else:
|
||||||
|
p = ((1048576.0 - raw_press) - (var2 / 4096.0)) * 6250.0 / var1
|
||||||
|
var1 = self.dig_P9 * p * p / 2147483648.0
|
||||||
|
var2 = p * self.dig_P8 / 32768.0
|
||||||
|
pressure = p + (var1 + var2 + self.dig_P7) / 16.0
|
||||||
|
pressure = max(30000, min(110000, pressure))
|
||||||
|
|
||||||
|
# humidity
|
||||||
|
h = (self.t_fine - 76800.0)
|
||||||
|
h = ((raw_hum - (self.dig_H4 * 64.0 + self.dig_H5 / 16384.0 * h)) *
|
||||||
|
(self.dig_H2 / 65536.0 * (1.0 + self.dig_H6 / 67108864.0 * h *
|
||||||
|
(1.0 + self.dig_H3 / 67108864.0 * h))))
|
||||||
|
humidity = h * (1.0 - self.dig_H1 * h / 524288.0)
|
||||||
|
# humidity = max(0, min(100, humidity))
|
||||||
|
|
||||||
|
if result:
|
||||||
|
result[0] = temp
|
||||||
|
result[1] = pressure
|
||||||
|
result[2] = humidity
|
||||||
|
return result
|
||||||
|
|
||||||
|
return array("f", (temp, pressure, humidity))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sealevel(self):
|
||||||
|
return self.__sealevel
|
||||||
|
|
||||||
|
@sealevel.setter
|
||||||
|
def sealevel(self, value):
|
||||||
|
if 30000 < value < 120000: # just ensure some reasonable value
|
||||||
|
self.__sealevel = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def altitude(self):
|
||||||
|
'''
|
||||||
|
Altitude in m.
|
||||||
|
'''
|
||||||
|
from math import pow
|
||||||
|
try:
|
||||||
|
p = 44330 * (1.0 - pow(self.read_compensated_data()[1] /
|
||||||
|
self.__sealevel, 0.1903))
|
||||||
|
except:
|
||||||
|
p = 0.0
|
||||||
|
return p
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dew_point(self):
|
||||||
|
"""
|
||||||
|
Compute the dew point temperature for the current Temperature
|
||||||
|
and Humidity measured pair
|
||||||
|
"""
|
||||||
|
from math import log
|
||||||
|
t, p, h = self.read_compensated_data()
|
||||||
|
h = (log(h, 10) - 2) / 0.4343 + (17.62 * t) / (243.12 + t)
|
||||||
|
return 243.12 * h / (17.62 - h)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def values(self):
|
||||||
|
""" human readable values """
|
||||||
|
|
||||||
|
t, p, h = self.read_compensated_data()
|
||||||
|
|
||||||
|
return ("{:.2f}C".format(t), "{:.2f}hPa".format(p/100),
|
||||||
|
"{:.2f}%".format(h))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature(self):
|
||||||
|
""" float in °C """
|
||||||
|
t, _, _ = self.read_compensated_data()
|
||||||
|
return t
|
||||||
|
@property
|
||||||
|
def pressure(self):
|
||||||
|
""" float in hPa """
|
||||||
|
_, p, _ = self.read_compensated_data()
|
||||||
|
return p
|
1
code/lib/homie/__init__.py
Normal file
1
code/lib/homie/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__version__ = "3.0.1"
|
50
code/lib/homie/constants.py
Normal file
50
code/lib/homie/constants.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from micropython import const
|
||||||
|
|
||||||
|
# Device
|
||||||
|
QOS = const(1)
|
||||||
|
MAIN_DELAY = const(1000)
|
||||||
|
STATS_DELAY = const(60000)
|
||||||
|
WDT_DELAY = const(100)
|
||||||
|
|
||||||
|
# Device states
|
||||||
|
STATE_INIT = "init"
|
||||||
|
STATE_READY = "ready"
|
||||||
|
STATE_RECOVER = "recover"
|
||||||
|
STATE_OTA = "ota"
|
||||||
|
STATE_WEBREPL = "webrepl"
|
||||||
|
|
||||||
|
# Property datatypes
|
||||||
|
STRING = "string"
|
||||||
|
ENUM = "enum"
|
||||||
|
BOOLEAN = "boolean"
|
||||||
|
INTEGER = "integer"
|
||||||
|
FLOAT = "float"
|
||||||
|
COLOR = "color"
|
||||||
|
|
||||||
|
# Property formats
|
||||||
|
RGB = "rgb"
|
||||||
|
HSV = "hsv"
|
||||||
|
|
||||||
|
# (Sub)Topics
|
||||||
|
DEVICE_STATE = "$state"
|
||||||
|
T_BC = "$broadcast"
|
||||||
|
T_MPY = "$mpy"
|
||||||
|
T_SET = "/set"
|
||||||
|
|
||||||
|
# General
|
||||||
|
UTF8 = "utf-8"
|
||||||
|
SET = "set"
|
||||||
|
SLASH = "/"
|
||||||
|
UNDERSCORE = "_"
|
||||||
|
|
||||||
|
ON = "on"
|
||||||
|
OFF = "off"
|
||||||
|
TRUE = "true"
|
||||||
|
FALSE = "false"
|
||||||
|
LOCKED = "locked"
|
||||||
|
UNLOCKED = "unlocked"
|
||||||
|
|
||||||
|
# Build-in extension strings
|
||||||
|
EXT_MPY = "org.microhomie.mpy:0.1.0:[4.x]"
|
||||||
|
EXT_FW = "org.homie.legacy-firmware:0.1.1:[4.x]"
|
||||||
|
EXT_STATS = "org.homie.legacy-stats:0.1.1:[4.x]"
|
334
code/lib/homie/device.py
Normal file
334
code/lib/homie/device.py
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
import uasyncio as asyncio
|
||||||
|
|
||||||
|
from gc import collect, mem_free
|
||||||
|
from sys import platform
|
||||||
|
|
||||||
|
from homie import __version__
|
||||||
|
from homie.network import get_local_ip, get_local_mac
|
||||||
|
from homie.constants import (
|
||||||
|
DEVICE_STATE,
|
||||||
|
MAIN_DELAY,
|
||||||
|
QOS,
|
||||||
|
SLASH,
|
||||||
|
STATE_OTA,
|
||||||
|
STATE_INIT,
|
||||||
|
STATE_READY,
|
||||||
|
STATE_RECOVER,
|
||||||
|
STATE_WEBREPL,
|
||||||
|
T_BC,
|
||||||
|
T_MPY,
|
||||||
|
T_SET,
|
||||||
|
UNDERSCORE,
|
||||||
|
UTF8,
|
||||||
|
WDT_DELAY,
|
||||||
|
EXT_MPY,
|
||||||
|
EXT_FW,
|
||||||
|
EXT_STATS,
|
||||||
|
)
|
||||||
|
from machine import RTC, reset
|
||||||
|
from mqtt_as import LINUX, MQTTClient
|
||||||
|
from uasyncio import sleep_ms
|
||||||
|
from ubinascii import hexlify
|
||||||
|
from utime import time
|
||||||
|
from primitives import launch
|
||||||
|
from primitives.message import Message
|
||||||
|
|
||||||
|
|
||||||
|
def get_unique_id():
|
||||||
|
if LINUX is False:
|
||||||
|
from machine import unique_id
|
||||||
|
return hexlify(unique_id()).decode()
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Linux doesn't have a unique id. Provide the DEVICE_ID option in your settings.py."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Decorator to block async tasks until the device is in "ready" state
|
||||||
|
_MESSAGE = Message()
|
||||||
|
def await_ready_state(func):
|
||||||
|
def new_gen(*args, **kwargs):
|
||||||
|
# fmt: off
|
||||||
|
await _MESSAGE
|
||||||
|
await func(*args, **kwargs)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
return new_gen
|
||||||
|
|
||||||
|
|
||||||
|
class HomieDevice:
|
||||||
|
|
||||||
|
"""MicroPython implementation of the Homie MQTT convention for IoT."""
|
||||||
|
|
||||||
|
def __init__(self, settings):
|
||||||
|
self.debug = getattr(settings, "DEBUG", False)
|
||||||
|
|
||||||
|
self._state = STATE_INIT
|
||||||
|
self._version = __version__
|
||||||
|
self._fw_name = "Microhomie"
|
||||||
|
self._extensions = getattr(settings, "EXTENSIONS", [])
|
||||||
|
self._bc_enabled = getattr(settings, "BROADCAST", False)
|
||||||
|
self._wifi = getattr(settings, "WIFI_CREDENTIALS", False)
|
||||||
|
|
||||||
|
self.first_start = True
|
||||||
|
self.stats_interval = getattr(settings, "DEVICE_STATS_INTERVAL", 60)
|
||||||
|
self.device_name = getattr(settings, "DEVICE_NAME", "")
|
||||||
|
self.callback_topics = {}
|
||||||
|
|
||||||
|
# Registered homie nodes
|
||||||
|
self.nodes = []
|
||||||
|
|
||||||
|
# Generate unique id if settings has no DEVICE_ID
|
||||||
|
self.device_id = getattr(settings, "DEVICE_ID", get_unique_id())
|
||||||
|
|
||||||
|
# Base topic
|
||||||
|
self.btopic = getattr(settings, "MQTT_BASE_TOPIC", "homie")
|
||||||
|
# Device base topic
|
||||||
|
self.dtopic = "{}/{}".format(self.btopic, self.device_id)
|
||||||
|
|
||||||
|
# mqtt_as client
|
||||||
|
self.mqtt = MQTTClient(
|
||||||
|
client_id=self.device_id,
|
||||||
|
server=settings.MQTT_BROKER,
|
||||||
|
port=getattr(settings, "MQTT_PORT", 1883),
|
||||||
|
user=getattr(settings, "MQTT_USERNAME", None),
|
||||||
|
password=getattr(settings, "MQTT_PASSWORD", None),
|
||||||
|
keepalive=getattr(settings, "MQTT_KEEPALIVE", 30),
|
||||||
|
ping_interval=getattr(settings, "MQTT_PING_INTERVAL", 0),
|
||||||
|
ssl=getattr(settings, "MQTT_SSL", False),
|
||||||
|
ssl_params=getattr(settings, "MQTT_SSL_PARAMS", {}),
|
||||||
|
response_time=getattr(settings, "MQTT_RESPONSE_TIME", 10),
|
||||||
|
clean_init=getattr(settings, "MQTT_CLEAN_INIT", True),
|
||||||
|
clean=getattr(settings, "MQTT_CLEAN", True),
|
||||||
|
max_repubs=getattr(settings, "MQTT_MAX_REPUBS", 4),
|
||||||
|
will=("{}/{}".format(self.dtopic, DEVICE_STATE), "lost", True, QOS),
|
||||||
|
subs_cb=self.subs_cb,
|
||||||
|
wifi_coro=None,
|
||||||
|
connect_coro=self.connection_handler,
|
||||||
|
ssid=getattr(settings, "WIFI_SSID", None),
|
||||||
|
wifi_pw=getattr(settings, "WIFI_PASSWORD", None),
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_node(self, node):
|
||||||
|
node.device = self
|
||||||
|
node.set_topic() # set topic for node properties
|
||||||
|
_p = node.properties
|
||||||
|
for p in _p:
|
||||||
|
p.set_topic()
|
||||||
|
self.nodes.append(node)
|
||||||
|
|
||||||
|
def all_properties(self, func, tup_args):
|
||||||
|
""" Run method on all registered property objects """
|
||||||
|
_n = self.nodes
|
||||||
|
for n in _n:
|
||||||
|
_p = n.properties
|
||||||
|
for p in _p:
|
||||||
|
_f = getattr(p, func)
|
||||||
|
launch(_f, tup_args)
|
||||||
|
|
||||||
|
async def subscribe(self, topic):
|
||||||
|
self.dprint("MQTT SUBSCRIBE: {}".format(topic))
|
||||||
|
await self.mqtt.subscribe(topic, QOS)
|
||||||
|
|
||||||
|
async def unsubscribe(self, topic):
|
||||||
|
self.dprint("MQTT UNSUBSCRIBE: {}".format(topic))
|
||||||
|
await self.mqtt.unsubscribe(topic)
|
||||||
|
|
||||||
|
async def connection_handler(self, client):
|
||||||
|
"""subscribe to all registered device and node topics"""
|
||||||
|
if not self.first_start:
|
||||||
|
await self.publish("{}/{}".format(self.dtopic, DEVICE_STATE), STATE_RECOVER)
|
||||||
|
|
||||||
|
# Subscribe to Homie broadcast topic
|
||||||
|
if self._bc_enabled:
|
||||||
|
await self.subscribe("{}/{}/#".format(self.btopic, T_BC))
|
||||||
|
|
||||||
|
# Subscribe to the Micropython extension topic
|
||||||
|
if EXT_MPY in self._extensions:
|
||||||
|
await self.subscribe("{}/{}".format(self.dtopic, T_MPY))
|
||||||
|
|
||||||
|
# Subscribe to node property topics
|
||||||
|
self.all_properties("subscribe", ())
|
||||||
|
|
||||||
|
# on first connection:
|
||||||
|
# * publish device and node properties
|
||||||
|
# * enable WDT
|
||||||
|
# * run all coros
|
||||||
|
if self.first_start is True:
|
||||||
|
await self.publish_properties()
|
||||||
|
|
||||||
|
# Unsubscribe from retained topics that received no retained message
|
||||||
|
for t in self.callback_topics:
|
||||||
|
if not t.endswith(T_SET):
|
||||||
|
await self.unsubscribe(t)
|
||||||
|
del self.callback_topics[t]
|
||||||
|
|
||||||
|
# Activate watchdog timer
|
||||||
|
if not LINUX and not self.debug:
|
||||||
|
asyncio.create_task(self.wdt())
|
||||||
|
|
||||||
|
# Start all async tasks decorated with await_ready_state
|
||||||
|
_MESSAGE.set()
|
||||||
|
await sleep_ms(MAIN_DELAY)
|
||||||
|
_MESSAGE.clear()
|
||||||
|
|
||||||
|
# Do not run this if clause again on wifi/broker reconnect
|
||||||
|
self.first_start = False
|
||||||
|
|
||||||
|
# Publish data from all properties on first start
|
||||||
|
self.all_properties("publish", ())
|
||||||
|
|
||||||
|
# Announce that the device is ready
|
||||||
|
await self.publish("{}/{}".format(self.dtopic, DEVICE_STATE), STATE_READY)
|
||||||
|
|
||||||
|
def subs_cb(self, topic, payload, retained):
|
||||||
|
""" The main callback for all subscribed topics """
|
||||||
|
topic = topic.decode()
|
||||||
|
payload = payload.decode()
|
||||||
|
|
||||||
|
self.dprint(
|
||||||
|
"MQTT MESSAGE: {} --> {}, {}".format(topic, payload, retained)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only non-retained messages are allowed on /set topics
|
||||||
|
if retained and topic.endswith(T_SET):
|
||||||
|
return
|
||||||
|
|
||||||
|
# broadcast topic
|
||||||
|
if T_BC in topic:
|
||||||
|
self.broadcast_callback(topic, payload, retained)
|
||||||
|
# Micropython extension
|
||||||
|
elif topic.endswith(T_MPY) and EXT_MPY in self._extensions:
|
||||||
|
if payload == "reset":
|
||||||
|
asyncio.create_task(self.reset("reset"))
|
||||||
|
elif payload == "webrepl":
|
||||||
|
asyncio.create_task(self.reset("webrepl"))
|
||||||
|
elif payload == "yaota8266" and platform == "esp8266":
|
||||||
|
asyncio.create_task(self.reset("yaotaota"))
|
||||||
|
# All other topics
|
||||||
|
else:
|
||||||
|
if topic in self.callback_topics:
|
||||||
|
self.callback_topics[topic](topic, payload, retained)
|
||||||
|
|
||||||
|
async def publish(self, topic, payload, retain=True):
|
||||||
|
if isinstance(payload, int):
|
||||||
|
payload = str(payload).encode()
|
||||||
|
|
||||||
|
if isinstance(payload, str):
|
||||||
|
payload = payload.encode()
|
||||||
|
|
||||||
|
self.dprint("MQTT PUBLISH: {} --> {}".format(topic, payload))
|
||||||
|
await self.mqtt.publish(topic, payload, retain, QOS)
|
||||||
|
|
||||||
|
async def broadcast(self, payload, level=None):
|
||||||
|
if isinstance(payload, int):
|
||||||
|
payload = str(payload)
|
||||||
|
|
||||||
|
topic = "{}/{}".format(self.btopic, T_BC)
|
||||||
|
if level is not None:
|
||||||
|
topic = "{}/{}".format(topic, level)
|
||||||
|
self.dprint("MQTT BROADCAST: {} --> {}".format(topic, payload))
|
||||||
|
await self.mqtt.publish(topic, payload, retain=False, qos=QOS)
|
||||||
|
|
||||||
|
def broadcast_callback(self, topic, payload, retained):
|
||||||
|
""" Gets called when the broadcast topic receives a message """
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def publish_properties(self):
|
||||||
|
""" Publish device and node properties """
|
||||||
|
_t = self.dtopic
|
||||||
|
publish = self.publish
|
||||||
|
|
||||||
|
# device properties
|
||||||
|
await publish("{}/$homie".format(_t), "4.0.0")
|
||||||
|
await publish("{}/$name".format(_t), self.device_name)
|
||||||
|
await publish("{}/{}".format(_t, DEVICE_STATE), STATE_INIT)
|
||||||
|
await publish("{}/$implementation".format(_t), bytes(platform, UTF8))
|
||||||
|
await publish(
|
||||||
|
"{}/$nodes".format(_t), ",".join([n.id for n in self.nodes])
|
||||||
|
)
|
||||||
|
|
||||||
|
# node properties
|
||||||
|
_n = self.nodes
|
||||||
|
for n in _n:
|
||||||
|
await n.publish_properties()
|
||||||
|
|
||||||
|
# extensions
|
||||||
|
await publish("{}/$extensions".format(_t), ",".join(self._extensions))
|
||||||
|
if EXT_FW in self._extensions:
|
||||||
|
await publish("{}/$localip".format(_t), get_local_ip())
|
||||||
|
await publish("{}/$mac".format(_t), get_local_mac())
|
||||||
|
await publish("{}/$fw/name".format(_t), self._fw_name)
|
||||||
|
await publish("{}/$fw/version".format(_t), self._version)
|
||||||
|
if EXT_STATS in self._extensions:
|
||||||
|
await self.publish("{}/$stats/interval".format(_t), str(self.stats_interval))
|
||||||
|
# Start stats coro
|
||||||
|
asyncio.create_task(self.publish_stats())
|
||||||
|
|
||||||
|
@await_ready_state
|
||||||
|
async def publish_stats(self):
|
||||||
|
from utime import time
|
||||||
|
|
||||||
|
_d = self.stats_interval * 1000 # delay
|
||||||
|
_st = time() # start time
|
||||||
|
_tup = "{}/$stats/uptime".format(self.dtopic) # Uptime topic
|
||||||
|
_tfh = "{}/$stats/freeheap".format(self.dtopic) # Freeheap topic
|
||||||
|
|
||||||
|
publish = self.publish
|
||||||
|
while True:
|
||||||
|
uptime = time() - _st
|
||||||
|
await publish(_tup, str(uptime))
|
||||||
|
await publish(_tfh, str(mem_free()))
|
||||||
|
await sleep_ms(_d)
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
if self._wifi:
|
||||||
|
await self.setup_wifi()
|
||||||
|
await self.mqtt.connect()
|
||||||
|
while True:
|
||||||
|
collect()
|
||||||
|
await sleep_ms(MAIN_DELAY)
|
||||||
|
except OSError:
|
||||||
|
print("ERROR: can not connect to MQTT")
|
||||||
|
await sleep_ms(5000)
|
||||||
|
|
||||||
|
def run_forever(self):
|
||||||
|
if RTC().memory() == b"webrepl":
|
||||||
|
RTC().memory(b"")
|
||||||
|
else:
|
||||||
|
asyncio.run(self.run())
|
||||||
|
|
||||||
|
async def reset(self, reason):
|
||||||
|
if reason != "reset":
|
||||||
|
RTC().memory(reason)
|
||||||
|
await self.publish("{}/{}".format(self.dtopic, DEVICE_STATE), reason)
|
||||||
|
await self.mqtt.disconnect()
|
||||||
|
await sleep_ms(500)
|
||||||
|
reset()
|
||||||
|
|
||||||
|
async def wdt(self):
|
||||||
|
from machine import WDT
|
||||||
|
|
||||||
|
wdt = WDT()
|
||||||
|
while True:
|
||||||
|
wdt.feed()
|
||||||
|
await sleep_ms(WDT_DELAY)
|
||||||
|
|
||||||
|
def dprint(self, *args):
|
||||||
|
if self.debug:
|
||||||
|
print(*args)
|
||||||
|
|
||||||
|
async def setup_wifi(self):
|
||||||
|
from homie.network import get_wifi_credentials
|
||||||
|
while True:
|
||||||
|
wifi_cfg = get_wifi_credentials(self._wifi)
|
||||||
|
if wifi_cfg is None:
|
||||||
|
self.dprint("No WiFi found. Rescanning...")
|
||||||
|
await sleep_ms(MAIN_DELAY)
|
||||||
|
else:
|
||||||
|
self.dprint("Connect to SSID: {}".format(wifi_cfg[0]))
|
||||||
|
self.mqtt._ssid = wifi_cfg[0]
|
||||||
|
self.mqtt._wifi_pw = wifi_cfg[1]
|
||||||
|
return
|
46
code/lib/homie/network.py
Normal file
46
code/lib/homie/network.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from mqtt_as import LINUX
|
||||||
|
|
||||||
|
|
||||||
|
if LINUX is False:
|
||||||
|
from network import WLAN, AP_IF, STA_IF
|
||||||
|
from ubinascii import hexlify
|
||||||
|
|
||||||
|
|
||||||
|
def enable_ap():
|
||||||
|
"""Disables any Accesspoint"""
|
||||||
|
wlan = WLAN(AP_IF)
|
||||||
|
wlan.active(True)
|
||||||
|
print("NETWORK: Access Point enabled.")
|
||||||
|
|
||||||
|
|
||||||
|
def disable_ap():
|
||||||
|
"""Disables any Accesspoint"""
|
||||||
|
wlan = WLAN(AP_IF)
|
||||||
|
wlan.active(False)
|
||||||
|
print("NETWORK: Access Point disabled.")
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_ip():
|
||||||
|
try:
|
||||||
|
return bytes(WLAN(0).ifconfig()[0], "utf-8")
|
||||||
|
except NameError:
|
||||||
|
return b"127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
|
def get_local_mac():
|
||||||
|
try:
|
||||||
|
return hexlify(WLAN(0).config("mac"), ":")
|
||||||
|
except NameError:
|
||||||
|
return b"00:00:00:00:00:00"
|
||||||
|
|
||||||
|
|
||||||
|
def get_wifi_credentials(wifi):
|
||||||
|
wlan = WLAN(STA_IF)
|
||||||
|
ssids = wlan.scan()
|
||||||
|
|
||||||
|
for s in ssids:
|
||||||
|
ssid = s[0].decode()
|
||||||
|
if ssid in wifi:
|
||||||
|
return (ssid, wifi[ssid])
|
||||||
|
|
||||||
|
return None
|
47
code/lib/homie/node.py
Normal file
47
code/lib/homie/node.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import uasyncio as asyncio
|
||||||
|
|
||||||
|
|
||||||
|
class BaseNode:
|
||||||
|
def __init__(self, id, name, type):
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.type = type
|
||||||
|
self.topic = None
|
||||||
|
self.device = None
|
||||||
|
self.properties = []
|
||||||
|
|
||||||
|
def set_topic(self):
|
||||||
|
self.topic = "{}/{}".format(
|
||||||
|
self.device.dtopic,
|
||||||
|
self.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_property(self, p, cb=None):
|
||||||
|
p.node = self
|
||||||
|
self.properties.append(p)
|
||||||
|
|
||||||
|
if cb:
|
||||||
|
p.on_message = cb
|
||||||
|
|
||||||
|
async def publish_properties(self):
|
||||||
|
"""General properties of this node"""
|
||||||
|
publish = self.device.publish
|
||||||
|
|
||||||
|
# Publish name and type
|
||||||
|
await publish("{}/$name".format(self.topic), self.name)
|
||||||
|
await publish("{}/$type".format(self.topic), self.type)
|
||||||
|
|
||||||
|
# Publish properties registerd with the node
|
||||||
|
properties = self.properties
|
||||||
|
await publish(
|
||||||
|
"{}/$properties".format(self.topic),
|
||||||
|
",".join([p.id for p in properties]),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Publish registerd properties
|
||||||
|
for p in properties:
|
||||||
|
await p.publish_properties()
|
||||||
|
|
||||||
|
|
||||||
|
# Keep for backward compatibility
|
||||||
|
HomieNode = BaseNode
|
137
code/lib/homie/property.py
Normal file
137
code/lib/homie/property.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import uasyncio as asyncio
|
||||||
|
|
||||||
|
from homie.constants import STRING, T_SET, TRUE, FALSE
|
||||||
|
from homie.validator import payload_is_valid
|
||||||
|
|
||||||
|
|
||||||
|
class BaseProperty:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
id,
|
||||||
|
name=None,
|
||||||
|
settable=False,
|
||||||
|
retained=True,
|
||||||
|
unit=None,
|
||||||
|
datatype=STRING,
|
||||||
|
format=None,
|
||||||
|
default=None,
|
||||||
|
restore=True,
|
||||||
|
on_message=None,
|
||||||
|
):
|
||||||
|
self._value = default
|
||||||
|
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.settable = settable
|
||||||
|
self.retained = retained
|
||||||
|
self.unit = unit
|
||||||
|
self.datatype = datatype
|
||||||
|
self.format = format
|
||||||
|
self.restore = restore
|
||||||
|
self.on_message = on_message
|
||||||
|
|
||||||
|
self.topic = None
|
||||||
|
self.node = None
|
||||||
|
|
||||||
|
# Keep for backward compatibility
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
# Keep for backward compatibility
|
||||||
|
@data.setter
|
||||||
|
def data(self, value):
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value):
|
||||||
|
""" Set value if changed and publish to mqtt """
|
||||||
|
if value != self._value:
|
||||||
|
self._value = value
|
||||||
|
self.publish()
|
||||||
|
|
||||||
|
def set_topic(self):
|
||||||
|
self.topic = "{}/{}/{}".format(
|
||||||
|
self.node.device.dtopic,
|
||||||
|
self.node.id,
|
||||||
|
self.id
|
||||||
|
)
|
||||||
|
|
||||||
|
def publish(self):
|
||||||
|
asyncio.create_task(
|
||||||
|
self.node.device.publish(
|
||||||
|
self.topic,
|
||||||
|
self.value,
|
||||||
|
self.retained
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def subscribe(self):
|
||||||
|
# Restore from topic with retained message on device start
|
||||||
|
if self.restore and self.node.device.first_start is True:
|
||||||
|
self.node.device.callback_topics[self.topic] = self.restore_handler
|
||||||
|
await self.node.device.subscribe(self.topic)
|
||||||
|
|
||||||
|
# Subscribe to settable (/set) topics
|
||||||
|
if self.settable is True:
|
||||||
|
topic = "{}/set".format(self.topic)
|
||||||
|
self.node.device.callback_topics[topic] = self.message_handler
|
||||||
|
await self.node.device.subscribe(topic)
|
||||||
|
|
||||||
|
def restore_handler(self, topic, payload, retained):
|
||||||
|
""" Gets called when the property should be restored from mqtt """
|
||||||
|
# Retained messages are not allowed on /set topics
|
||||||
|
if topic.endswith(T_SET):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Unsubscribe from topic and remove the callback handler
|
||||||
|
asyncio.create_task(self.node.device.unsubscribe(topic))
|
||||||
|
del self.node.device.callback_topics[topic]
|
||||||
|
|
||||||
|
if payload_is_valid(self, payload):
|
||||||
|
if payload != self._value:
|
||||||
|
if self.on_message:
|
||||||
|
self.on_message(topic, payload, retained)
|
||||||
|
|
||||||
|
self._value = payload
|
||||||
|
|
||||||
|
def message_handler(self, topic, payload, retained):
|
||||||
|
""" Gets called when the property receive a message on /set topic """
|
||||||
|
# No reatained messages allowed on /set topics
|
||||||
|
if retained:
|
||||||
|
return
|
||||||
|
|
||||||
|
if payload_is_valid(self, payload):
|
||||||
|
if self.on_message:
|
||||||
|
self.on_message(topic, payload, retained)
|
||||||
|
|
||||||
|
self.value = payload
|
||||||
|
|
||||||
|
async def publish_properties(self):
|
||||||
|
topic = self.topic
|
||||||
|
publish = self.node.device.publish
|
||||||
|
|
||||||
|
await publish("{}/$name".format(topic), self.name)
|
||||||
|
await publish("{}/$datatype".format(topic), self.datatype)
|
||||||
|
|
||||||
|
if self.format is not None:
|
||||||
|
await publish("{}/$format".format(topic), self.format)
|
||||||
|
|
||||||
|
if self.settable is True:
|
||||||
|
await publish("{}/$settable".format(topic), TRUE)
|
||||||
|
|
||||||
|
if self.retained is False:
|
||||||
|
await publish("{}/$retained".format(topic), FALSE)
|
||||||
|
|
||||||
|
if self.unit is not None:
|
||||||
|
await publish("{}/$unit".format(topic), self.unit)
|
||||||
|
|
||||||
|
|
||||||
|
HomieProperty = BaseProperty
|
||||||
|
|
||||||
|
# Keep for backward compatibility
|
||||||
|
HomieNodeProperty = BaseProperty
|
54
code/lib/homie/validator.py
Normal file
54
code/lib/homie/validator.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from homie.constants import (
|
||||||
|
BOOLEAN,
|
||||||
|
COLOR,
|
||||||
|
ENUM,
|
||||||
|
FALSE,
|
||||||
|
FLOAT,
|
||||||
|
INTEGER,
|
||||||
|
STRING,
|
||||||
|
TRUE,
|
||||||
|
RGB,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def payload_is_valid(cls, payload):
|
||||||
|
_dt = cls.datatype
|
||||||
|
_fmt = cls.format
|
||||||
|
|
||||||
|
if _dt == STRING:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif _dt == INTEGER:
|
||||||
|
try:
|
||||||
|
i = int(payload)
|
||||||
|
if _fmt != None:
|
||||||
|
first, last = _fmt.split(":")
|
||||||
|
first = int(first)
|
||||||
|
last = int(last)
|
||||||
|
|
||||||
|
if i < first or i > last:
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif _dt == FLOAT:
|
||||||
|
try:
|
||||||
|
float(payload)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif _dt == BOOLEAN:
|
||||||
|
if payload != TRUE and payload != FALSE:
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif _dt == ENUM:
|
||||||
|
_values = cls.format.split(",")
|
||||||
|
if payload not in _values:
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif _dt == COLOR:
|
||||||
|
if _fmt == RGB:
|
||||||
|
if len(payload.split(",")) != 3:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
698
code/lib/mqtt_as.py
Normal file
698
code/lib/mqtt_as.py
Normal file
@ -0,0 +1,698 @@
|
|||||||
|
# mqtt_as.py Asynchronous version of umqtt.robust
|
||||||
|
# (C) Copyright Peter Hinch 2017-2019.
|
||||||
|
# (C) Copyright Kevin Köck 2018-2019.
|
||||||
|
# Released under the MIT licence.
|
||||||
|
|
||||||
|
# Pyboard D support added
|
||||||
|
# Various improvements contributed by Kevin Köck.
|
||||||
|
|
||||||
|
import gc
|
||||||
|
import usocket as socket
|
||||||
|
import ustruct as struct
|
||||||
|
|
||||||
|
gc.collect()
|
||||||
|
from ubinascii import hexlify
|
||||||
|
import uasyncio as asyncio
|
||||||
|
|
||||||
|
gc.collect()
|
||||||
|
from utime import ticks_ms, ticks_diff
|
||||||
|
from uerrno import EINPROGRESS, ETIMEDOUT
|
||||||
|
|
||||||
|
gc.collect()
|
||||||
|
from micropython import const
|
||||||
|
|
||||||
|
gc.collect()
|
||||||
|
from sys import platform
|
||||||
|
|
||||||
|
VERSION = (0, 6, 0)
|
||||||
|
|
||||||
|
# Default short delay for good SynCom throughput (avoid sleep(0) with SynCom).
|
||||||
|
_DEFAULT_MS = const(20)
|
||||||
|
_SOCKET_POLL_DELAY = const(5) # 100ms added greatly to publish latency
|
||||||
|
|
||||||
|
# Legitimate errors while waiting on a socket. See uasyncio __init__.py open_connection().
|
||||||
|
if platform == 'esp32' or platform == 'esp32_LoBo':
|
||||||
|
# https://forum.micropython.org/viewtopic.php?f=16&t=3608&p=20942#p20942
|
||||||
|
BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT, 118, 119] # Add in weird ESP32 errors
|
||||||
|
else:
|
||||||
|
BUSY_ERRORS = [EINPROGRESS, ETIMEDOUT]
|
||||||
|
|
||||||
|
ESP8266 = platform == 'esp8266'
|
||||||
|
ESP32 = platform == 'esp32'
|
||||||
|
PYBOARD = platform == 'pyboard'
|
||||||
|
LOBO = platform == 'esp32_LoBo'
|
||||||
|
LINUX = platform == "linux"
|
||||||
|
|
||||||
|
if LINUX is False:
|
||||||
|
import network
|
||||||
|
from machine import unique_id
|
||||||
|
else:
|
||||||
|
def unique_id():
|
||||||
|
raise NotImplementedError("Linux doesn't have a unique id. Provide the argument client_id")
|
||||||
|
|
||||||
|
|
||||||
|
# Default "do little" coro for optional user replacement
|
||||||
|
async def eliza(*_): # e.g. via set_wifi_handler(coro): see test program
|
||||||
|
await asyncio.sleep_ms(_DEFAULT_MS)
|
||||||
|
|
||||||
|
|
||||||
|
class MQTTException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def pid_gen():
|
||||||
|
pid = 0
|
||||||
|
while True:
|
||||||
|
pid = pid + 1 if pid < 65535 else 1
|
||||||
|
yield pid
|
||||||
|
|
||||||
|
|
||||||
|
def qos_check(qos):
|
||||||
|
if not (qos == 0 or qos == 1):
|
||||||
|
raise ValueError('Only qos 0 and 1 are supported.')
|
||||||
|
|
||||||
|
|
||||||
|
# MQTT_base class. Handles MQTT protocol on the basis of a good connection.
|
||||||
|
# Exceptions from connectivity failures are handled by MQTTClient subclass.
|
||||||
|
class MQTT_base:
|
||||||
|
REPUB_COUNT = 0 # TEST
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
def __init__(self, client_id, server, port, user, password, keepalive, ping_interval,
|
||||||
|
ssl, ssl_params, response_time, clean_init, clean, max_repubs, will,
|
||||||
|
subs_cb, wifi_coro, connect_coro, ssid, wifi_pw):
|
||||||
|
# MQTT config
|
||||||
|
self.ping_interval = ping_interval
|
||||||
|
self._client_id = client_id
|
||||||
|
self._user = user
|
||||||
|
self._pswd = password
|
||||||
|
self._keepalive = keepalive
|
||||||
|
if self._keepalive >= 65536:
|
||||||
|
raise ValueError('invalid keepalive time')
|
||||||
|
self._response_time = response_time * 1000 # Repub if no PUBACK received (ms).
|
||||||
|
self._max_repubs = max_repubs
|
||||||
|
self._clean_init = clean_init # clean_session state on first connection
|
||||||
|
self._clean = clean # clean_session state on reconnect
|
||||||
|
if will is None:
|
||||||
|
self._lw_topic = False
|
||||||
|
else:
|
||||||
|
self._set_last_will(*will)
|
||||||
|
# WiFi config
|
||||||
|
self._ssid = ssid # Required ESP32 / Pyboard D
|
||||||
|
self._wifi_pw = wifi_pw
|
||||||
|
self._ssl = ssl
|
||||||
|
self._ssl_params = ssl_params
|
||||||
|
# Callbacks and coros
|
||||||
|
self._cb = subs_cb
|
||||||
|
self._wifi_handler = wifi_coro
|
||||||
|
self._connect_handler = connect_coro
|
||||||
|
# Network
|
||||||
|
self.port = port
|
||||||
|
if self.port == 0:
|
||||||
|
self.port = 8883 if self._ssl else 1883
|
||||||
|
self.server = server
|
||||||
|
if self.server is None:
|
||||||
|
raise ValueError('no server specified.')
|
||||||
|
self._sock = None
|
||||||
|
if LINUX is True:
|
||||||
|
self._sta_isconnected = True
|
||||||
|
else:
|
||||||
|
self._sta_if = network.WLAN(network.STA_IF)
|
||||||
|
self._sta_if.active(True)
|
||||||
|
|
||||||
|
self.newpid = pid_gen()
|
||||||
|
self.rcv_pids = set() # PUBACK and SUBACK pids awaiting ACK response
|
||||||
|
self.last_rx = ticks_ms() # Time of last communication from broker
|
||||||
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
|
def _set_last_will(self, topic, msg, retain=False, qos=0):
|
||||||
|
qos_check(qos)
|
||||||
|
if not topic:
|
||||||
|
raise ValueError('Empty topic.')
|
||||||
|
self._lw_topic = topic
|
||||||
|
self._lw_msg = msg
|
||||||
|
self._lw_qos = qos
|
||||||
|
self._lw_retain = retain
|
||||||
|
|
||||||
|
def dprint(self, *args):
|
||||||
|
if self.DEBUG:
|
||||||
|
print(*args)
|
||||||
|
|
||||||
|
def _timeout(self, t):
|
||||||
|
return ticks_diff(ticks_ms(), t) > self._response_time
|
||||||
|
|
||||||
|
async def _as_read(self, n, sock=None): # OSError caught by superclass
|
||||||
|
if sock is None:
|
||||||
|
sock = self._sock
|
||||||
|
data = b''
|
||||||
|
t = ticks_ms()
|
||||||
|
while len(data) < n:
|
||||||
|
if self._timeout(t) or not self.isconnected():
|
||||||
|
raise OSError(-1)
|
||||||
|
try:
|
||||||
|
msg = sock.read(n - len(data))
|
||||||
|
except OSError as e: # ESP32 issues weird 119 errors here
|
||||||
|
msg = None
|
||||||
|
if e.args[0] not in BUSY_ERRORS:
|
||||||
|
raise
|
||||||
|
if msg == b'': # Connection closed by host
|
||||||
|
raise OSError(-1)
|
||||||
|
if msg is not None: # data received
|
||||||
|
data = b''.join((data, msg))
|
||||||
|
t = ticks_ms()
|
||||||
|
self.last_rx = ticks_ms()
|
||||||
|
await asyncio.sleep_ms(_SOCKET_POLL_DELAY)
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def _as_write(self, bytes_wr, length=0, sock=None):
|
||||||
|
if sock is None:
|
||||||
|
sock = self._sock
|
||||||
|
if length:
|
||||||
|
bytes_wr = bytes_wr[:length]
|
||||||
|
t = ticks_ms()
|
||||||
|
while bytes_wr:
|
||||||
|
if self._timeout(t) or not self.isconnected():
|
||||||
|
raise OSError(-1)
|
||||||
|
try:
|
||||||
|
n = sock.write(bytes_wr)
|
||||||
|
except OSError as e: # ESP32 issues weird 119 errors here
|
||||||
|
n = 0
|
||||||
|
if e.args[0] not in BUSY_ERRORS:
|
||||||
|
raise
|
||||||
|
if n:
|
||||||
|
t = ticks_ms()
|
||||||
|
bytes_wr = bytes_wr[n:]
|
||||||
|
await asyncio.sleep_ms(_SOCKET_POLL_DELAY)
|
||||||
|
|
||||||
|
async def _send_str(self, s):
|
||||||
|
await self._as_write(struct.pack("!H", len(s)))
|
||||||
|
await self._as_write(s)
|
||||||
|
|
||||||
|
async def _recv_len(self):
|
||||||
|
n = 0
|
||||||
|
sh = 0
|
||||||
|
while 1:
|
||||||
|
res = await self._as_read(1)
|
||||||
|
b = res[0]
|
||||||
|
n |= (b & 0x7f) << sh
|
||||||
|
if not b & 0x80:
|
||||||
|
return n
|
||||||
|
sh += 7
|
||||||
|
|
||||||
|
async def _connect(self, clean):
|
||||||
|
self._sock = socket.socket()
|
||||||
|
self._sock.setblocking(False)
|
||||||
|
try:
|
||||||
|
self._sock.connect(self._addr)
|
||||||
|
except OSError as e:
|
||||||
|
if e.args[0] not in BUSY_ERRORS:
|
||||||
|
raise
|
||||||
|
await asyncio.sleep_ms(_DEFAULT_MS)
|
||||||
|
self.dprint('Connecting to broker.')
|
||||||
|
if self._ssl:
|
||||||
|
import ussl
|
||||||
|
self._sock = ussl.wrap_socket(self._sock, **self._ssl_params)
|
||||||
|
premsg = bytearray(b"\x10\0\0\0\0\0")
|
||||||
|
msg = bytearray(b"\x04MQTT\x04\0\0\0") # Protocol 3.1.1
|
||||||
|
|
||||||
|
sz = 10 + 2 + len(self._client_id)
|
||||||
|
msg[6] = clean << 1
|
||||||
|
if self._user:
|
||||||
|
sz += 2 + len(self._user) + 2 + len(self._pswd)
|
||||||
|
msg[6] |= 0xC0
|
||||||
|
if self._keepalive:
|
||||||
|
msg[7] |= self._keepalive >> 8
|
||||||
|
msg[8] |= self._keepalive & 0x00FF
|
||||||
|
if self._lw_topic:
|
||||||
|
sz += 2 + len(self._lw_topic) + 2 + len(self._lw_msg)
|
||||||
|
msg[6] |= 0x4 | (self._lw_qos & 0x1) << 3 | (self._lw_qos & 0x2) << 3
|
||||||
|
msg[6] |= self._lw_retain << 5
|
||||||
|
|
||||||
|
i = 1
|
||||||
|
while sz > 0x7f:
|
||||||
|
premsg[i] = (sz & 0x7f) | 0x80
|
||||||
|
sz >>= 7
|
||||||
|
i += 1
|
||||||
|
premsg[i] = sz
|
||||||
|
await self._as_write(premsg, i + 2)
|
||||||
|
await self._as_write(msg)
|
||||||
|
await self._send_str(self._client_id)
|
||||||
|
if self._lw_topic:
|
||||||
|
await self._send_str(self._lw_topic)
|
||||||
|
await self._send_str(self._lw_msg)
|
||||||
|
if self._user:
|
||||||
|
await self._send_str(self._user)
|
||||||
|
await self._send_str(self._pswd)
|
||||||
|
# Await CONNACK
|
||||||
|
# read causes ECONNABORTED if broker is out; triggers a reconnect.
|
||||||
|
resp = await self._as_read(4)
|
||||||
|
self.dprint('Connected to broker.') # Got CONNACK
|
||||||
|
if resp[3] != 0 or resp[0] != 0x20 or resp[1] != 0x02:
|
||||||
|
raise OSError(-1) # Bad CONNACK e.g. authentication fail.
|
||||||
|
|
||||||
|
async def _ping(self):
|
||||||
|
async with self.lock:
|
||||||
|
await self._as_write(b"\xc0\0")
|
||||||
|
|
||||||
|
# Check internet connectivity by sending DNS lookup to Google's 8.8.8.8
|
||||||
|
async def wan_ok(self,
|
||||||
|
packet=b'$\x1a\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01'):
|
||||||
|
if not self.isconnected(): # WiFi is down
|
||||||
|
return False
|
||||||
|
length = 32 # DNS query and response packet size
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
s.setblocking(False)
|
||||||
|
s.connect(('8.8.8.8', 53))
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
try:
|
||||||
|
await self._as_write(packet, sock=s)
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
res = await self._as_read(length, s)
|
||||||
|
if len(res) == length:
|
||||||
|
return True # DNS response size OK
|
||||||
|
except OSError: # Timeout on read: no connectivity.
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
s.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def broker_up(self): # Test broker connectivity
|
||||||
|
if not self.isconnected():
|
||||||
|
return False
|
||||||
|
tlast = self.last_rx
|
||||||
|
if ticks_diff(ticks_ms(), tlast) < 1000:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
await self._ping()
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
t = ticks_ms()
|
||||||
|
while not self._timeout(t):
|
||||||
|
await asyncio.sleep_ms(100)
|
||||||
|
if ticks_diff(self.last_rx, tlast) > 0: # Response received
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def disconnect(self):
|
||||||
|
try:
|
||||||
|
async with self.lock:
|
||||||
|
self._sock.write(b"\xe0\0")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
self._has_connected = False
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self._sock is not None:
|
||||||
|
self._sock.close()
|
||||||
|
|
||||||
|
async def _await_pid(self, pid):
|
||||||
|
t = ticks_ms()
|
||||||
|
while pid in self.rcv_pids: # local copy
|
||||||
|
if self._timeout(t) or not self.isconnected():
|
||||||
|
break # Must repub or bail out
|
||||||
|
await asyncio.sleep_ms(100)
|
||||||
|
else:
|
||||||
|
return True # PID received. All done.
|
||||||
|
return False
|
||||||
|
|
||||||
|
# qos == 1: coro blocks until wait_msg gets correct PID.
|
||||||
|
# If WiFi fails completely subclass re-publishes with new PID.
|
||||||
|
async def publish(self, topic, msg, retain, qos):
|
||||||
|
pid = next(self.newpid)
|
||||||
|
if qos:
|
||||||
|
self.rcv_pids.add(pid)
|
||||||
|
async with self.lock:
|
||||||
|
await self._publish(topic, msg, retain, qos, 0, pid)
|
||||||
|
if qos == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
while 1: # Await PUBACK, republish on timeout
|
||||||
|
if await self._await_pid(pid):
|
||||||
|
return
|
||||||
|
# No match
|
||||||
|
if count >= self._max_repubs or not self.isconnected():
|
||||||
|
raise OSError(-1) # Subclass to re-publish with new PID
|
||||||
|
async with self.lock:
|
||||||
|
await self._publish(topic, msg, retain, qos, dup=1, pid=pid) # Add pid
|
||||||
|
count += 1
|
||||||
|
self.REPUB_COUNT += 1
|
||||||
|
|
||||||
|
async def _publish(self, topic, msg, retain, qos, dup, pid):
|
||||||
|
pkt = bytearray(b"\x30\0\0\0")
|
||||||
|
pkt[0] |= qos << 1 | retain | dup << 3
|
||||||
|
sz = 2 + len(topic) + len(msg)
|
||||||
|
if qos > 0:
|
||||||
|
sz += 2
|
||||||
|
if sz >= 2097152:
|
||||||
|
raise MQTTException('Strings too long.')
|
||||||
|
i = 1
|
||||||
|
while sz > 0x7f:
|
||||||
|
pkt[i] = (sz & 0x7f) | 0x80
|
||||||
|
sz >>= 7
|
||||||
|
i += 1
|
||||||
|
pkt[i] = sz
|
||||||
|
await self._as_write(pkt, i + 1)
|
||||||
|
await self._send_str(topic)
|
||||||
|
if qos > 0:
|
||||||
|
struct.pack_into("!H", pkt, 0, pid)
|
||||||
|
await self._as_write(pkt, 2)
|
||||||
|
await self._as_write(msg)
|
||||||
|
|
||||||
|
# Can raise OSError if WiFi fails. Subclass traps
|
||||||
|
async def subscribe(self, topic, qos):
|
||||||
|
pkt = bytearray(b"\x82\0\0\0")
|
||||||
|
pid = next(self.newpid)
|
||||||
|
self.rcv_pids.add(pid)
|
||||||
|
struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, pid)
|
||||||
|
async with self.lock:
|
||||||
|
await self._as_write(pkt)
|
||||||
|
await self._send_str(topic)
|
||||||
|
await self._as_write(qos.to_bytes(1, "little"))
|
||||||
|
|
||||||
|
if not await self._await_pid(pid):
|
||||||
|
raise OSError(-1)
|
||||||
|
|
||||||
|
# Can raise OSError if WiFi fails. Subclass traps
|
||||||
|
async def unsubscribe(self, topic):
|
||||||
|
pkt = bytearray(b"\xa2\0\0\0")
|
||||||
|
pid = next(self.newpid)
|
||||||
|
self.rcv_pids.add(pid)
|
||||||
|
struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic), pid)
|
||||||
|
async with self.lock:
|
||||||
|
await self._as_write(pkt)
|
||||||
|
await self._send_str(topic)
|
||||||
|
|
||||||
|
if not await self._await_pid(pid):
|
||||||
|
raise OSError(-1)
|
||||||
|
|
||||||
|
# Wait for a single incoming MQTT message and process it.
|
||||||
|
# Subscribed messages are delivered to a callback previously
|
||||||
|
# set by .setup() method. Other (internal) MQTT
|
||||||
|
# messages processed internally.
|
||||||
|
# Immediate return if no data available. Called from ._handle_msg().
|
||||||
|
async def wait_msg(self):
|
||||||
|
res = self._sock.read(1) # Throws OSError on WiFi fail
|
||||||
|
if res is None:
|
||||||
|
return
|
||||||
|
if res == b'':
|
||||||
|
raise OSError(-1)
|
||||||
|
|
||||||
|
if res == b"\xd0": # PINGRESP
|
||||||
|
await self._as_read(1) # Update .last_rx time
|
||||||
|
return
|
||||||
|
op = res[0]
|
||||||
|
|
||||||
|
if op == 0x40: # PUBACK: save pid
|
||||||
|
sz = await self._as_read(1)
|
||||||
|
if sz != b"\x02":
|
||||||
|
raise OSError(-1)
|
||||||
|
rcv_pid = await self._as_read(2)
|
||||||
|
pid = rcv_pid[0] << 8 | rcv_pid[1]
|
||||||
|
if pid in self.rcv_pids:
|
||||||
|
self.rcv_pids.discard(pid)
|
||||||
|
else:
|
||||||
|
raise OSError(-1)
|
||||||
|
|
||||||
|
if op == 0x90: # SUBACK
|
||||||
|
resp = await self._as_read(4)
|
||||||
|
if resp[3] == 0x80:
|
||||||
|
raise OSError(-1)
|
||||||
|
pid = resp[2] | (resp[1] << 8)
|
||||||
|
if pid in self.rcv_pids:
|
||||||
|
self.rcv_pids.discard(pid)
|
||||||
|
else:
|
||||||
|
raise OSError(-1)
|
||||||
|
|
||||||
|
if op == 0xB0: # UNSUBACK
|
||||||
|
resp = await self._as_read(3)
|
||||||
|
pid = resp[2] | (resp[1] << 8)
|
||||||
|
if pid in self.rcv_pids:
|
||||||
|
self.rcv_pids.discard(pid)
|
||||||
|
else:
|
||||||
|
raise OSError(-1)
|
||||||
|
|
||||||
|
if op & 0xf0 != 0x30:
|
||||||
|
return
|
||||||
|
sz = await self._recv_len()
|
||||||
|
topic_len = await self._as_read(2)
|
||||||
|
topic_len = (topic_len[0] << 8) | topic_len[1]
|
||||||
|
topic = await self._as_read(topic_len)
|
||||||
|
sz -= topic_len + 2
|
||||||
|
if op & 6:
|
||||||
|
pid = await self._as_read(2)
|
||||||
|
pid = pid[0] << 8 | pid[1]
|
||||||
|
sz -= 2
|
||||||
|
msg = await self._as_read(sz)
|
||||||
|
retained = op & 0x01
|
||||||
|
self._cb(topic, msg, bool(retained))
|
||||||
|
if op & 6 == 2: # qos 1
|
||||||
|
pkt = bytearray(b"\x40\x02\0\0") # Send PUBACK
|
||||||
|
struct.pack_into("!H", pkt, 2, pid)
|
||||||
|
await self._as_write(pkt)
|
||||||
|
elif op & 6 == 4: # qos 2 not supported
|
||||||
|
raise OSError(-1)
|
||||||
|
|
||||||
|
|
||||||
|
# MQTTClient class. Handles issues relating to connectivity.
|
||||||
|
|
||||||
|
class MQTTClient(MQTT_base):
|
||||||
|
def __init__(self, client_id=None,
|
||||||
|
server=None,
|
||||||
|
port=0,
|
||||||
|
user='',
|
||||||
|
password='',
|
||||||
|
keepalive=60,
|
||||||
|
ping_interval=0,
|
||||||
|
ssl=False,
|
||||||
|
ssl_params={},
|
||||||
|
response_time=10,
|
||||||
|
clean_init=True,
|
||||||
|
clean=True,
|
||||||
|
max_repubs=4,
|
||||||
|
will=None,
|
||||||
|
subs_cb=lambda *_: None,
|
||||||
|
wifi_coro=None,
|
||||||
|
connect_coro=None,
|
||||||
|
ssid=None,
|
||||||
|
wifi_pw=None):
|
||||||
|
client_id = client_id or hexlify(unique_id())
|
||||||
|
wifi_coro = wifi_coro or eliza
|
||||||
|
connect_coro = connect_coro or eliza
|
||||||
|
super().__init__(client_id, server, port, user, password, keepalive, ping_interval,
|
||||||
|
ssl, ssl_params, response_time, clean_init, clean, max_repubs, will,
|
||||||
|
subs_cb, wifi_coro, connect_coro, ssid, wifi_pw)
|
||||||
|
self._isconnected = False # Current connection state
|
||||||
|
keepalive = 1000 * self._keepalive # ms
|
||||||
|
self._ping_interval = keepalive // 4 if keepalive else 20000
|
||||||
|
p_i = self.ping_interval * 1000 # Can specify shorter e.g. for subscribe-only
|
||||||
|
if p_i and p_i < self._ping_interval:
|
||||||
|
self._ping_interval = p_i
|
||||||
|
self._in_connect = False
|
||||||
|
self._has_connected = False # Define 'Clean Session' value to use.
|
||||||
|
if ESP8266:
|
||||||
|
import esp
|
||||||
|
esp.sleep_type(0) # Improve connection integrity at cost of power consumption.
|
||||||
|
|
||||||
|
async def wifi_connect(self):
|
||||||
|
if LINUX is True: # no network control, assume connected as OS takes care of that
|
||||||
|
self._sta_isconnected = True
|
||||||
|
return
|
||||||
|
s = self._sta_if
|
||||||
|
if ESP8266:
|
||||||
|
if s.isconnected(): # 1st attempt, already connected.
|
||||||
|
return
|
||||||
|
s.active(True)
|
||||||
|
s.connect() # ESP8266 remembers connection.
|
||||||
|
for _ in range(60):
|
||||||
|
if s.status() != network.STAT_CONNECTING: # Break out on fail or success. Check once per sec.
|
||||||
|
break
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
if s.status() == network.STAT_CONNECTING: # might hang forever awaiting dhcp lease renewal or something else
|
||||||
|
s.disconnect()
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
if not s.isconnected() and self._ssid is not None and self._wifi_pw is not None:
|
||||||
|
s.connect(self._ssid, self._wifi_pw)
|
||||||
|
while s.status() == network.STAT_CONNECTING: # Break out on fail or success. Check once per sec.
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
else:
|
||||||
|
s.active(True)
|
||||||
|
s.connect(self._ssid, self._wifi_pw)
|
||||||
|
if PYBOARD: # Doesn't yet have STAT_CONNECTING constant
|
||||||
|
while s.status() in (1, 2):
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
elif LOBO:
|
||||||
|
i = 0
|
||||||
|
while not s.isconnected():
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
i += 1
|
||||||
|
if i >= 10:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
while s.status() == network.STAT_CONNECTING: # Break out on fail or success. Check once per sec.
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
if not s.isconnected():
|
||||||
|
raise OSError
|
||||||
|
# Ensure connection stays up for a few secs.
|
||||||
|
self.dprint('Checking WiFi integrity.')
|
||||||
|
for _ in range(5):
|
||||||
|
if not s.isconnected():
|
||||||
|
raise OSError # in 1st 5 secs
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
self.dprint('Got reliable connection')
|
||||||
|
|
||||||
|
async def connect(self):
|
||||||
|
if not self._has_connected:
|
||||||
|
await self.wifi_connect() # On 1st call, caller handles error
|
||||||
|
# Note this blocks if DNS lookup occurs. Do it once to prevent
|
||||||
|
# blocking during later internet outage:
|
||||||
|
self._addr = socket.getaddrinfo(self.server, self.port)[0][-1]
|
||||||
|
self._in_connect = True # Disable low level ._isconnected check
|
||||||
|
clean = self._clean if self._has_connected else self._clean_init
|
||||||
|
try:
|
||||||
|
await self._connect(clean)
|
||||||
|
except Exception:
|
||||||
|
self.close()
|
||||||
|
raise
|
||||||
|
self.rcv_pids.clear()
|
||||||
|
# If we get here without error broker/LAN must be up.
|
||||||
|
self._isconnected = True
|
||||||
|
self._in_connect = False # Low level code can now check connectivity.
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.create_task(self._wifi_handler(True)) # User handler.
|
||||||
|
if not self._has_connected:
|
||||||
|
self._has_connected = True # Use normal clean flag on reconnect.
|
||||||
|
loop.create_task(
|
||||||
|
self._keep_connected()) # Runs forever unless user issues .disconnect()
|
||||||
|
|
||||||
|
loop.create_task(self._handle_msg()) # Tasks quit on connection fail.
|
||||||
|
loop.create_task(self._keep_alive())
|
||||||
|
if self.DEBUG:
|
||||||
|
loop.create_task(self._memory())
|
||||||
|
loop.create_task(self._connect_handler(self)) # User handler.
|
||||||
|
|
||||||
|
# Launched by .connect(). Runs until connectivity fails. Checks for and
|
||||||
|
# handles incoming messages.
|
||||||
|
async def _handle_msg(self):
|
||||||
|
try:
|
||||||
|
while self.isconnected():
|
||||||
|
async with self.lock:
|
||||||
|
await self.wait_msg() # Immediate return if no message
|
||||||
|
await asyncio.sleep_ms(_DEFAULT_MS) # Let other tasks get lock
|
||||||
|
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
self._reconnect() # Broker or WiFi fail.
|
||||||
|
|
||||||
|
# Keep broker alive MQTT spec 3.1.2.10 Keep Alive.
|
||||||
|
# Runs until ping failure or no response in keepalive period.
|
||||||
|
async def _keep_alive(self):
|
||||||
|
while self.isconnected():
|
||||||
|
pings_due = ticks_diff(ticks_ms(), self.last_rx) // self._ping_interval
|
||||||
|
if pings_due >= 4:
|
||||||
|
self.dprint('Reconnect: broker fail.')
|
||||||
|
break
|
||||||
|
elif pings_due >= 1:
|
||||||
|
try:
|
||||||
|
await self._ping()
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
self._reconnect() # Broker or WiFi fail.
|
||||||
|
|
||||||
|
# DEBUG: show RAM messages.
|
||||||
|
async def _memory(self):
|
||||||
|
count = 0
|
||||||
|
while self.isconnected(): # Ensure just one instance.
|
||||||
|
await asyncio.sleep(1) # Quick response to outage.
|
||||||
|
count += 1
|
||||||
|
count %= 20
|
||||||
|
if not count:
|
||||||
|
gc.collect()
|
||||||
|
print('RAM free {} alloc {}'.format(gc.mem_free(), gc.mem_alloc()))
|
||||||
|
|
||||||
|
def isconnected(self):
|
||||||
|
if self._in_connect: # Disable low-level check during .connect()
|
||||||
|
return True
|
||||||
|
if LINUX is True:
|
||||||
|
if self._isconnected and self._sta_isconnected is False:
|
||||||
|
self._reconnect()
|
||||||
|
else:
|
||||||
|
if self._isconnected and not self._sta_if.isconnected(): # It's going down.
|
||||||
|
self._reconnect()
|
||||||
|
return self._isconnected
|
||||||
|
|
||||||
|
def _reconnect(self): # Schedule a reconnection if not underway.
|
||||||
|
if self._isconnected:
|
||||||
|
self._isconnected = False
|
||||||
|
self.close()
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.create_task(self._wifi_handler(False)) # User handler.
|
||||||
|
|
||||||
|
# Await broker connection.
|
||||||
|
async def _connection(self):
|
||||||
|
while not self._isconnected:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
# Scheduled on 1st successful connection. Runs forever maintaining wifi and
|
||||||
|
# broker connection. Must handle conditions at edge of WiFi range.
|
||||||
|
async def _keep_connected(self):
|
||||||
|
while self._has_connected:
|
||||||
|
if self.isconnected(): # Pause for 1 second
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
gc.collect()
|
||||||
|
else:
|
||||||
|
if LINUX is True:
|
||||||
|
self._sta_isconnected = False
|
||||||
|
else:
|
||||||
|
self._sta_if.disconnect()
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
try:
|
||||||
|
await self.wifi_connect()
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if not self._has_connected: # User has issued the terminal .disconnect()
|
||||||
|
self.dprint('Disconnected, exiting _keep_connected')
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
await self.connect()
|
||||||
|
# Now has set ._isconnected and scheduled _connect_handler().
|
||||||
|
self.dprint('Reconnect OK!')
|
||||||
|
except OSError as e:
|
||||||
|
self.dprint('Error in reconnect.', e)
|
||||||
|
# Can get ECONNABORTED or -1. The latter signifies no or bad CONNACK received.
|
||||||
|
self.close() # Disconnect and try again.
|
||||||
|
self._in_connect = False
|
||||||
|
self._isconnected = False
|
||||||
|
self.dprint('Disconnected, exited _keep_connected')
|
||||||
|
|
||||||
|
async def subscribe(self, topic, qos=0):
|
||||||
|
qos_check(qos)
|
||||||
|
while 1:
|
||||||
|
await self._connection()
|
||||||
|
try:
|
||||||
|
return await super().subscribe(topic, qos)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
self._reconnect() # Broker or WiFi fail.
|
||||||
|
|
||||||
|
async def unsubscribe(self, topic):
|
||||||
|
while 1:
|
||||||
|
await self._connection()
|
||||||
|
try:
|
||||||
|
return await super().unsubscribe(topic)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
self._reconnect() # Broker or WiFi fail.
|
||||||
|
|
||||||
|
async def publish(self, topic, msg, retain=False, qos=0):
|
||||||
|
qos_check(qos)
|
||||||
|
while 1:
|
||||||
|
await self._connection()
|
||||||
|
try:
|
||||||
|
return await super().publish(topic, msg, retain, qos)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
self._reconnect() # Broker or WiFi fail.
|
31
code/lib/primitives/__init__.py
Normal file
31
code/lib/primitives/__init__.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# __init__.py Common functions for uasyncio primitives
|
||||||
|
|
||||||
|
# Copyright (c) 2018-2020 Peter Hinch
|
||||||
|
# Released under the MIT License (MIT) - see LICENSE file
|
||||||
|
|
||||||
|
try:
|
||||||
|
import uasyncio as asyncio
|
||||||
|
except ImportError:
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
|
async def _g():
|
||||||
|
pass
|
||||||
|
type_coro = type(_g())
|
||||||
|
|
||||||
|
# If a callback is passed, run it and return.
|
||||||
|
# If a coro is passed initiate it and return.
|
||||||
|
# coros are passed by name i.e. not using function call syntax.
|
||||||
|
def launch(func, tup_args):
|
||||||
|
res = func(*tup_args)
|
||||||
|
if isinstance(res, type_coro):
|
||||||
|
res = asyncio.create_task(res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def set_global_exception():
|
||||||
|
def _handle_exception(loop, context):
|
||||||
|
import sys
|
||||||
|
sys.print_exception(context["exception"])
|
||||||
|
sys.exit()
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.set_exception_handler(_handle_exception)
|
69
code/lib/primitives/delay_ms.py
Normal file
69
code/lib/primitives/delay_ms.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# delay_ms.py
|
||||||
|
|
||||||
|
# Copyright (c) 2018-2020 Peter Hinch
|
||||||
|
# Released under the MIT License (MIT) - see LICENSE file
|
||||||
|
# Rewritten for uasyncio V3. Allows stop time to be brought forwards.
|
||||||
|
|
||||||
|
import uasyncio as asyncio
|
||||||
|
from utime import ticks_add, ticks_diff, ticks_ms
|
||||||
|
from micropython import schedule
|
||||||
|
from . import launch
|
||||||
|
# Usage:
|
||||||
|
# from primitives.delay_ms import Delay_ms
|
||||||
|
|
||||||
|
class Delay_ms:
|
||||||
|
verbose = False # verbose and can_alloc retained to avoid breaking code.
|
||||||
|
def __init__(self, func=None, args=(), can_alloc=True, duration=1000):
|
||||||
|
self._func = func
|
||||||
|
self._args = args
|
||||||
|
self._duration = duration # Default duration
|
||||||
|
self._tstop = None # Stop time (ms). None signifies not running.
|
||||||
|
self._tsave = None # Temporary storage for stop time
|
||||||
|
self._ktask = None # timer task
|
||||||
|
self._retrn = None # Return value of launched callable
|
||||||
|
self._do_trig = self._trig # Avoid allocation in .trigger
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self._ktask is not None:
|
||||||
|
self._ktask.cancel()
|
||||||
|
|
||||||
|
def trigger(self, duration=0): # Update end time
|
||||||
|
now = ticks_ms()
|
||||||
|
if duration <= 0: # Use default set by constructor
|
||||||
|
duration = self._duration
|
||||||
|
self._retrn = None
|
||||||
|
is_running = self()
|
||||||
|
tstop = self._tstop # Current stop time
|
||||||
|
# Retriggering normally just updates ._tstop for ._timer
|
||||||
|
self._tstop = ticks_add(now, duration)
|
||||||
|
# Identify special case where we are bringing the end time forward
|
||||||
|
can = is_running and duration < ticks_diff(tstop, now)
|
||||||
|
if not is_running or can:
|
||||||
|
schedule(self._do_trig, can)
|
||||||
|
|
||||||
|
def _trig(self, can):
|
||||||
|
if can:
|
||||||
|
self._ktask.cancel()
|
||||||
|
self._ktask = asyncio.create_task(self._timer(can))
|
||||||
|
|
||||||
|
def __call__(self): # Current running status
|
||||||
|
return self._tstop is not None
|
||||||
|
|
||||||
|
running = __call__
|
||||||
|
|
||||||
|
def rvalue(self):
|
||||||
|
return self._retrn
|
||||||
|
|
||||||
|
async def _timer(self, restart):
|
||||||
|
if restart: # Restore cached end time
|
||||||
|
self._tstop = self._tsave
|
||||||
|
try:
|
||||||
|
twait = ticks_diff(self._tstop, ticks_ms())
|
||||||
|
while twait > 0: # Must loop here: might be retriggered
|
||||||
|
await asyncio.sleep_ms(twait)
|
||||||
|
twait = ticks_diff(self._tstop, ticks_ms())
|
||||||
|
if self._func is not None: # Timed out: execute callback
|
||||||
|
self._retrn = launch(self._func, self._args)
|
||||||
|
finally:
|
||||||
|
self._tsave = self._tstop # Save in case we restart.
|
||||||
|
self._tstop = None # timer is stopped
|
70
code/lib/primitives/message.py
Normal file
70
code/lib/primitives/message.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# message.py
|
||||||
|
|
||||||
|
# Copyright (c) 2018-2020 Peter Hinch
|
||||||
|
# Released under the MIT License (MIT) - see LICENSE file
|
||||||
|
|
||||||
|
try:
|
||||||
|
import uasyncio as asyncio
|
||||||
|
except ImportError:
|
||||||
|
import asyncio
|
||||||
|
# Usage:
|
||||||
|
# from primitives.message import Message
|
||||||
|
|
||||||
|
# A coro waiting on a message issues await message
|
||||||
|
# A coro rasing the message issues message.set(payload)
|
||||||
|
# When all waiting coros have run
|
||||||
|
# message.clear() should be issued
|
||||||
|
|
||||||
|
# This more efficient version is commented out because Event.set is not ISR
|
||||||
|
# friendly. TODO If it gets fixed, reinstate this (tested) version and update
|
||||||
|
# tutorial for 1:n operation.
|
||||||
|
#class Message(asyncio.Event):
|
||||||
|
#def __init__(self, _=0):
|
||||||
|
#self._data = None
|
||||||
|
#super().__init__()
|
||||||
|
|
||||||
|
#def clear(self):
|
||||||
|
#self._data = None
|
||||||
|
#super().clear()
|
||||||
|
|
||||||
|
#def __await__(self):
|
||||||
|
#await super().wait()
|
||||||
|
|
||||||
|
#__iter__ = __await__
|
||||||
|
|
||||||
|
#def set(self, data=None):
|
||||||
|
#self._data = data
|
||||||
|
#super().set()
|
||||||
|
|
||||||
|
#def value(self):
|
||||||
|
#return self._data
|
||||||
|
|
||||||
|
# Has an ISR-friendly .set()
|
||||||
|
class Message():
|
||||||
|
def __init__(self, delay_ms=0):
|
||||||
|
self.delay_ms = delay_ms
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self._flag = False
|
||||||
|
self._data = None
|
||||||
|
|
||||||
|
async def wait(self): # CPython comptaibility
|
||||||
|
while not self._flag:
|
||||||
|
await asyncio.sleep_ms(self.delay_ms)
|
||||||
|
|
||||||
|
def __await__(self):
|
||||||
|
while not self._flag:
|
||||||
|
await asyncio.sleep_ms(self.delay_ms)
|
||||||
|
|
||||||
|
__iter__ = __await__
|
||||||
|
|
||||||
|
def is_set(self):
|
||||||
|
return self._flag
|
||||||
|
|
||||||
|
def set(self, data=None):
|
||||||
|
self._flag = True
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
def value(self):
|
||||||
|
return self._data
|
102
code/lib/primitives/pushbutton.py
Normal file
102
code/lib/primitives/pushbutton.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# pushbutton.py
|
||||||
|
|
||||||
|
# Copyright (c) 2018-2020 Peter Hinch
|
||||||
|
# Released under the MIT License (MIT) - see LICENSE file
|
||||||
|
|
||||||
|
import uasyncio as asyncio
|
||||||
|
import utime as time
|
||||||
|
from . import launch
|
||||||
|
from primitives.delay_ms import Delay_ms
|
||||||
|
|
||||||
|
|
||||||
|
# An alternative Pushbutton solution with lower RAM use is available here
|
||||||
|
# https://github.com/kevinkk525/pysmartnode/blob/dev/pysmartnode/utils/abutton.py
|
||||||
|
class Pushbutton:
|
||||||
|
debounce_ms = 50
|
||||||
|
long_press_ms = 1000
|
||||||
|
double_click_ms = 400
|
||||||
|
def __init__(self, pin, suppress=False, sense=None):
|
||||||
|
self.pin = pin # Initialise for input
|
||||||
|
self._supp = suppress
|
||||||
|
self._dblpend = False # Doubleclick waiting for 2nd click
|
||||||
|
self._dblran = False # Doubleclick executed user function
|
||||||
|
self._tf = False
|
||||||
|
self._ff = False
|
||||||
|
self._df = False
|
||||||
|
self._lf = False
|
||||||
|
self._ld = False # Delay_ms instance for long press
|
||||||
|
self._dd = False # Ditto for doubleclick
|
||||||
|
self.sense = pin.value() if sense is None else sense # Convert from electrical to logical value
|
||||||
|
self.state = self.rawstate() # Initial state
|
||||||
|
asyncio.create_task(self.buttoncheck()) # Thread runs forever
|
||||||
|
|
||||||
|
def press_func(self, func, args=()):
|
||||||
|
self._tf = func
|
||||||
|
self._ta = args
|
||||||
|
|
||||||
|
def release_func(self, func, args=()):
|
||||||
|
self._ff = func
|
||||||
|
self._fa = args
|
||||||
|
|
||||||
|
def double_func(self, func, args=()):
|
||||||
|
self._df = func
|
||||||
|
self._da = args
|
||||||
|
|
||||||
|
def long_func(self, func, args=()):
|
||||||
|
self._lf = func
|
||||||
|
self._la = args
|
||||||
|
|
||||||
|
# Current non-debounced logical button state: True == pressed
|
||||||
|
def rawstate(self):
|
||||||
|
return bool(self.pin.value() ^ self.sense)
|
||||||
|
|
||||||
|
# Current debounced state of button (True == pressed)
|
||||||
|
def __call__(self):
|
||||||
|
return self.state
|
||||||
|
|
||||||
|
def _ddto(self): # Doubleclick timeout: no doubleclick occurred
|
||||||
|
self._dblpend = False
|
||||||
|
if self._supp and not self.state:
|
||||||
|
if not self._ld or (self._ld and not self._ld()):
|
||||||
|
launch(self._ff, self._fa)
|
||||||
|
|
||||||
|
async def buttoncheck(self):
|
||||||
|
if self._lf: # Instantiate timers if funcs exist
|
||||||
|
self._ld = Delay_ms(self._lf, self._la)
|
||||||
|
if self._df:
|
||||||
|
self._dd = Delay_ms(self._ddto)
|
||||||
|
while True:
|
||||||
|
state = self.rawstate()
|
||||||
|
# State has changed: act on it now.
|
||||||
|
if state != self.state:
|
||||||
|
self.state = state
|
||||||
|
if state: # Button pressed: launch pressed func
|
||||||
|
if self._tf:
|
||||||
|
launch(self._tf, self._ta)
|
||||||
|
if self._lf: # There's a long func: start long press delay
|
||||||
|
self._ld.trigger(Pushbutton.long_press_ms)
|
||||||
|
if self._df:
|
||||||
|
if self._dd(): # Second click: timer running
|
||||||
|
self._dd.stop()
|
||||||
|
self._dblpend = False
|
||||||
|
self._dblran = True # Prevent suppressed launch on release
|
||||||
|
launch(self._df, self._da)
|
||||||
|
else:
|
||||||
|
# First click: start doubleclick timer
|
||||||
|
self._dd.trigger(Pushbutton.double_click_ms)
|
||||||
|
self._dblpend = True # Prevent suppressed launch on release
|
||||||
|
else: # Button release. Is there a release func?
|
||||||
|
if self._ff:
|
||||||
|
if self._supp:
|
||||||
|
d = self._ld
|
||||||
|
# If long delay exists, is running and doubleclick status is OK
|
||||||
|
if not self._dblpend and not self._dblran:
|
||||||
|
if (d and d()) or not d:
|
||||||
|
launch(self._ff, self._fa)
|
||||||
|
else:
|
||||||
|
launch(self._ff, self._fa)
|
||||||
|
if self._ld:
|
||||||
|
self._ld.stop() # Avoid interpreting a second click as a long push
|
||||||
|
self._dblran = False
|
||||||
|
# Ignore state changes until switch has settled
|
||||||
|
await asyncio.sleep_ms(Pushbutton.debounce_ms)
|
42
code/lib/primitives/switch.py
Normal file
42
code/lib/primitives/switch.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# switch.py
|
||||||
|
|
||||||
|
# Copyright (c) 2018-2020 Peter Hinch
|
||||||
|
# Released under the MIT License (MIT) - see LICENSE file
|
||||||
|
|
||||||
|
import uasyncio as asyncio
|
||||||
|
import utime as time
|
||||||
|
from . import launch
|
||||||
|
|
||||||
|
class Switch:
|
||||||
|
debounce_ms = 50
|
||||||
|
def __init__(self, pin):
|
||||||
|
self.pin = pin # Should be initialised for input with pullup
|
||||||
|
self._open_func = False
|
||||||
|
self._close_func = False
|
||||||
|
self.switchstate = self.pin.value() # Get initial state
|
||||||
|
asyncio.create_task(self.switchcheck()) # Thread runs forever
|
||||||
|
|
||||||
|
def open_func(self, func, args=()):
|
||||||
|
self._open_func = func
|
||||||
|
self._open_args = args
|
||||||
|
|
||||||
|
def close_func(self, func, args=()):
|
||||||
|
self._close_func = func
|
||||||
|
self._close_args = args
|
||||||
|
|
||||||
|
# Return current state of switch (0 = pressed)
|
||||||
|
def __call__(self):
|
||||||
|
return self.switchstate
|
||||||
|
|
||||||
|
async def switchcheck(self):
|
||||||
|
while True:
|
||||||
|
state = self.pin.value()
|
||||||
|
if state != self.switchstate:
|
||||||
|
# State has changed: act on it now.
|
||||||
|
self.switchstate = state
|
||||||
|
if state == 0 and self._close_func:
|
||||||
|
launch(self._close_func, self._close_args)
|
||||||
|
elif state == 1 and self._open_func:
|
||||||
|
launch(self._open_func, self._open_args)
|
||||||
|
# Ignore further state changes until switch has settled
|
||||||
|
await asyncio.sleep_ms(Switch.debounce_ms)
|
227
code/lib/sh1106.py
Normal file
227
code/lib/sh1106.py
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
#
|
||||||
|
# MicroPython SH1106 OLED driver, I2C and SPI interfaces
|
||||||
|
#
|
||||||
|
# The MIT License (MIT)
|
||||||
|
#
|
||||||
|
# Copyright (c) 2016 Radomir Dopieralski (@deshipu),
|
||||||
|
# 2017 Robert Hammelrath (@robert-hh)
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
#
|
||||||
|
# Sample code sections
|
||||||
|
# ------------ SPI ------------------
|
||||||
|
# Pin Map SPI
|
||||||
|
# - 3v - xxxxxx - Vcc
|
||||||
|
# - G - xxxxxx - Gnd
|
||||||
|
# - D7 - GPIO 13 - Din / MOSI fixed
|
||||||
|
# - D5 - GPIO 14 - Clk / Sck fixed
|
||||||
|
# - D8 - GPIO 4 - CS (optional, if the only connected device)
|
||||||
|
# - D2 - GPIO 5 - D/C
|
||||||
|
# - D1 - GPIO 2 - Res
|
||||||
|
#
|
||||||
|
# for CS, D/C and Res other ports may be chosen.
|
||||||
|
#
|
||||||
|
# from machine import Pin, SPI
|
||||||
|
# import sh1106
|
||||||
|
|
||||||
|
# spi = SPI(1, baudrate=1000000)
|
||||||
|
# display = sh1106.SH1106_SPI(128, 64, spi, Pin(5), Pin(2), Pin(4))
|
||||||
|
# display.sleep(False)
|
||||||
|
# display.fill(0)
|
||||||
|
# display.text('Testing 1', 0, 0, 1)
|
||||||
|
# display.show()
|
||||||
|
#
|
||||||
|
# --------------- I2C ------------------
|
||||||
|
#
|
||||||
|
# Pin Map I2C
|
||||||
|
# - 3v - xxxxxx - Vcc
|
||||||
|
# - G - xxxxxx - Gnd
|
||||||
|
# - D2 - GPIO 5 - SCK / SCL
|
||||||
|
# - D1 - GPIO 4 - DIN / SDA
|
||||||
|
# - D0 - GPIO 16 - Res
|
||||||
|
# - G - xxxxxx CS
|
||||||
|
# - G - xxxxxx D/C
|
||||||
|
#
|
||||||
|
# Pin's for I2C can be set almost arbitrary
|
||||||
|
#
|
||||||
|
# from machine import Pin, I2C
|
||||||
|
# import sh1106
|
||||||
|
#
|
||||||
|
# i2c = I2C(scl=Pin(5), sda=Pin(4), freq=400000)
|
||||||
|
# display = sh1106.SH1106_I2C(128, 64, i2c, Pin(16), 0x3c)
|
||||||
|
# display.sleep(False)
|
||||||
|
# display.fill(0)
|
||||||
|
# display.text('Testing 1', 0, 0, 1)
|
||||||
|
# display.show()
|
||||||
|
|
||||||
|
from micropython import const
|
||||||
|
import utime as time
|
||||||
|
import framebuf
|
||||||
|
|
||||||
|
|
||||||
|
# a few register definitions
|
||||||
|
_SET_CONTRAST = const(0x81)
|
||||||
|
_SET_NORM_INV = const(0xa6)
|
||||||
|
_SET_DISP = const(0xae)
|
||||||
|
_SET_SCAN_DIR = const(0xc0)
|
||||||
|
_SET_SEG_REMAP = const(0xa0)
|
||||||
|
_LOW_COLUMN_ADDRESS = const(0x00)
|
||||||
|
_HIGH_COLUMN_ADDRESS = const(0x10)
|
||||||
|
_SET_PAGE_ADDRESS = const(0xB0)
|
||||||
|
|
||||||
|
|
||||||
|
class SH1106:
|
||||||
|
def __init__(self, width, height, external_vcc):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.external_vcc = external_vcc
|
||||||
|
self.pages = self.height // 8
|
||||||
|
self.buffer = bytearray(self.pages * self.width)
|
||||||
|
fb = framebuf.FrameBuffer(self.buffer, self.width, self.height,
|
||||||
|
framebuf.MVLSB)
|
||||||
|
self.framebuf = fb
|
||||||
|
# set shortcuts for the methods of framebuf
|
||||||
|
self.fill = fb.fill
|
||||||
|
self.fill_rect = fb.fill_rect
|
||||||
|
self.hline = fb.hline
|
||||||
|
self.vline = fb.vline
|
||||||
|
self.line = fb.line
|
||||||
|
self.rect = fb.rect
|
||||||
|
self.pixel = fb.pixel
|
||||||
|
self.scroll = fb.scroll
|
||||||
|
self.text = fb.text
|
||||||
|
self.blit = fb.blit
|
||||||
|
|
||||||
|
self.init_display()
|
||||||
|
|
||||||
|
def init_display(self):
|
||||||
|
self.reset()
|
||||||
|
self.fill(0)
|
||||||
|
self.poweron()
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def poweroff(self):
|
||||||
|
self.write_cmd(_SET_DISP | 0x00)
|
||||||
|
|
||||||
|
def poweron(self):
|
||||||
|
self.write_cmd(_SET_DISP | 0x01)
|
||||||
|
|
||||||
|
def rotate(self, flag, update=True):
|
||||||
|
if flag:
|
||||||
|
self.write_cmd(_SET_SEG_REMAP | 0x01) # mirror display vertically
|
||||||
|
self.write_cmd(_SET_SCAN_DIR | 0x08) # mirror display hor.
|
||||||
|
else:
|
||||||
|
self.write_cmd(_SET_SEG_REMAP | 0x00)
|
||||||
|
self.write_cmd(_SET_SCAN_DIR | 0x00)
|
||||||
|
if update:
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def sleep(self, value):
|
||||||
|
self.write_cmd(_SET_DISP | (not value))
|
||||||
|
|
||||||
|
def contrast(self, contrast):
|
||||||
|
self.write_cmd(_SET_CONTRAST)
|
||||||
|
self.write_cmd(contrast)
|
||||||
|
|
||||||
|
def invert(self, invert):
|
||||||
|
self.write_cmd(_SET_NORM_INV | (invert & 1))
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
for page in range(self.height // 8):
|
||||||
|
self.write_cmd(_SET_PAGE_ADDRESS | page)
|
||||||
|
self.write_cmd(_LOW_COLUMN_ADDRESS | 2)
|
||||||
|
self.write_cmd(_HIGH_COLUMN_ADDRESS | 0)
|
||||||
|
self.write_data(self.buffer[
|
||||||
|
self.width * page:self.width * page + self.width
|
||||||
|
])
|
||||||
|
|
||||||
|
def reset(self, res):
|
||||||
|
if res is not None:
|
||||||
|
res(1)
|
||||||
|
time.sleep_ms(1)
|
||||||
|
res(0)
|
||||||
|
time.sleep_ms(20)
|
||||||
|
res(1)
|
||||||
|
time.sleep_ms(20)
|
||||||
|
|
||||||
|
|
||||||
|
class SH1106_I2C(SH1106):
|
||||||
|
def __init__(self, width, height, i2c, res=None, addr=0x3c,
|
||||||
|
external_vcc=False):
|
||||||
|
self.i2c = i2c
|
||||||
|
self.addr = addr
|
||||||
|
self.res = res
|
||||||
|
self.temp = bytearray(2)
|
||||||
|
if res is not None:
|
||||||
|
res.init(res.OUT, value=1)
|
||||||
|
super().__init__(width, height, external_vcc)
|
||||||
|
|
||||||
|
def write_cmd(self, cmd):
|
||||||
|
self.temp[0] = 0x80 # Co=1, D/C#=0
|
||||||
|
self.temp[1] = cmd
|
||||||
|
self.i2c.writeto(self.addr, self.temp)
|
||||||
|
|
||||||
|
def write_data(self, buf):
|
||||||
|
self.i2c.writeto(self.addr, b'\x40'+buf)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
super().reset(self.res)
|
||||||
|
|
||||||
|
|
||||||
|
class SH1106_SPI(SH1106):
|
||||||
|
def __init__(self, width, height, spi, dc, res=None, cs=None,
|
||||||
|
external_vcc=False):
|
||||||
|
self.rate = 10 * 1000 * 1000
|
||||||
|
dc.init(dc.OUT, value=0)
|
||||||
|
if res is not None:
|
||||||
|
res.init(res.OUT, value=0)
|
||||||
|
if cs is not None:
|
||||||
|
cs.init(cs.OUT, value=1)
|
||||||
|
self.spi = spi
|
||||||
|
self.dc = dc
|
||||||
|
self.res = res
|
||||||
|
self.cs = cs
|
||||||
|
super().__init__(width, height, external_vcc)
|
||||||
|
|
||||||
|
def write_cmd(self, cmd):
|
||||||
|
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
|
||||||
|
if self.cs is not None:
|
||||||
|
self.cs(1)
|
||||||
|
self.dc(0)
|
||||||
|
self.cs(0)
|
||||||
|
self.spi.write(bytearray([cmd]))
|
||||||
|
self.cs(1)
|
||||||
|
else:
|
||||||
|
self.dc(0)
|
||||||
|
self.spi.write(bytearray([cmd]))
|
||||||
|
|
||||||
|
def write_data(self, buf):
|
||||||
|
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
|
||||||
|
if self.cs is not None:
|
||||||
|
self.cs(1)
|
||||||
|
self.dc(1)
|
||||||
|
self.cs(0)
|
||||||
|
self.spi.write(buf)
|
||||||
|
self.cs(1)
|
||||||
|
else:
|
||||||
|
self.dc(1)
|
||||||
|
self.spi.write(buf)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
super().reset(self.res)
|
155
code/lib/ssd1306.py
Normal file
155
code/lib/ssd1306.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
|
||||||
|
|
||||||
|
from micropython import const
|
||||||
|
import framebuf
|
||||||
|
|
||||||
|
|
||||||
|
# register definitions
|
||||||
|
SET_CONTRAST = const(0x81)
|
||||||
|
SET_ENTIRE_ON = const(0xA4)
|
||||||
|
SET_NORM_INV = const(0xA6)
|
||||||
|
SET_DISP = const(0xAE)
|
||||||
|
SET_MEM_ADDR = const(0x20)
|
||||||
|
SET_COL_ADDR = const(0x21)
|
||||||
|
SET_PAGE_ADDR = const(0x22)
|
||||||
|
SET_DISP_START_LINE = const(0x40)
|
||||||
|
SET_SEG_REMAP = const(0xA0)
|
||||||
|
SET_MUX_RATIO = const(0xA8)
|
||||||
|
SET_COM_OUT_DIR = const(0xC0)
|
||||||
|
SET_DISP_OFFSET = const(0xD3)
|
||||||
|
SET_COM_PIN_CFG = const(0xDA)
|
||||||
|
SET_DISP_CLK_DIV = const(0xD5)
|
||||||
|
SET_PRECHARGE = const(0xD9)
|
||||||
|
SET_VCOM_DESEL = const(0xDB)
|
||||||
|
SET_CHARGE_PUMP = const(0x8D)
|
||||||
|
|
||||||
|
# Subclassing FrameBuffer provides support for graphics primitives
|
||||||
|
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
|
||||||
|
class SSD1306(framebuf.FrameBuffer):
|
||||||
|
def __init__(self, width, height, external_vcc):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.external_vcc = external_vcc
|
||||||
|
self.pages = self.height // 8
|
||||||
|
self.buffer = bytearray(self.pages * self.width)
|
||||||
|
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
|
||||||
|
self.init_display()
|
||||||
|
|
||||||
|
def init_display(self):
|
||||||
|
for cmd in (
|
||||||
|
SET_DISP | 0x00, # off
|
||||||
|
# address setting
|
||||||
|
SET_MEM_ADDR,
|
||||||
|
0x00, # horizontal
|
||||||
|
# resolution and layout
|
||||||
|
SET_DISP_START_LINE | 0x00,
|
||||||
|
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
|
||||||
|
SET_MUX_RATIO,
|
||||||
|
self.height - 1,
|
||||||
|
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
|
||||||
|
SET_DISP_OFFSET,
|
||||||
|
0x00,
|
||||||
|
SET_COM_PIN_CFG,
|
||||||
|
0x02 if self.width > 2 * self.height else 0x12,
|
||||||
|
# timing and driving scheme
|
||||||
|
SET_DISP_CLK_DIV,
|
||||||
|
0x80,
|
||||||
|
SET_PRECHARGE,
|
||||||
|
0x22 if self.external_vcc else 0xF1,
|
||||||
|
SET_VCOM_DESEL,
|
||||||
|
0x30, # 0.83*Vcc
|
||||||
|
# display
|
||||||
|
SET_CONTRAST,
|
||||||
|
0xFF, # maximum
|
||||||
|
SET_ENTIRE_ON, # output follows RAM contents
|
||||||
|
SET_NORM_INV, # not inverted
|
||||||
|
# charge pump
|
||||||
|
SET_CHARGE_PUMP,
|
||||||
|
0x10 if self.external_vcc else 0x14,
|
||||||
|
SET_DISP | 0x01,
|
||||||
|
): # on
|
||||||
|
self.write_cmd(cmd)
|
||||||
|
self.fill(0)
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def poweroff(self):
|
||||||
|
self.write_cmd(SET_DISP | 0x00)
|
||||||
|
|
||||||
|
def poweron(self):
|
||||||
|
self.write_cmd(SET_DISP | 0x01)
|
||||||
|
|
||||||
|
def contrast(self, contrast):
|
||||||
|
self.write_cmd(SET_CONTRAST)
|
||||||
|
self.write_cmd(contrast)
|
||||||
|
|
||||||
|
def invert(self, invert):
|
||||||
|
self.write_cmd(SET_NORM_INV | (invert & 1))
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
x0 = 0
|
||||||
|
x1 = self.width - 1
|
||||||
|
if self.width == 64:
|
||||||
|
# displays with width of 64 pixels are shifted by 32
|
||||||
|
x0 += 32
|
||||||
|
x1 += 32
|
||||||
|
self.write_cmd(SET_COL_ADDR)
|
||||||
|
self.write_cmd(x0)
|
||||||
|
self.write_cmd(x1)
|
||||||
|
self.write_cmd(SET_PAGE_ADDR)
|
||||||
|
self.write_cmd(0)
|
||||||
|
self.write_cmd(self.pages - 1)
|
||||||
|
self.write_data(self.buffer)
|
||||||
|
|
||||||
|
|
||||||
|
class SSD1306_I2C(SSD1306):
|
||||||
|
def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
|
||||||
|
self.i2c = i2c
|
||||||
|
self.addr = addr
|
||||||
|
self.temp = bytearray(2)
|
||||||
|
self.write_list = [b"\x40", None] # Co=0, D/C#=1
|
||||||
|
super().__init__(width, height, external_vcc)
|
||||||
|
|
||||||
|
def write_cmd(self, cmd):
|
||||||
|
self.temp[0] = 0x80 # Co=1, D/C#=0
|
||||||
|
self.temp[1] = cmd
|
||||||
|
self.i2c.writeto(self.addr, self.temp)
|
||||||
|
|
||||||
|
def write_data(self, buf):
|
||||||
|
self.write_list[1] = buf
|
||||||
|
self.i2c.writevto(self.addr, self.write_list)
|
||||||
|
|
||||||
|
|
||||||
|
class SSD1306_SPI(SSD1306):
|
||||||
|
def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
|
||||||
|
self.rate = 10 * 1024 * 1024
|
||||||
|
dc.init(dc.OUT, value=0)
|
||||||
|
res.init(res.OUT, value=0)
|
||||||
|
cs.init(cs.OUT, value=1)
|
||||||
|
self.spi = spi
|
||||||
|
self.dc = dc
|
||||||
|
self.res = res
|
||||||
|
self.cs = cs
|
||||||
|
import time
|
||||||
|
|
||||||
|
self.res(1)
|
||||||
|
time.sleep_ms(1)
|
||||||
|
self.res(0)
|
||||||
|
time.sleep_ms(10)
|
||||||
|
self.res(1)
|
||||||
|
super().__init__(width, height, external_vcc)
|
||||||
|
|
||||||
|
def write_cmd(self, cmd):
|
||||||
|
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
|
||||||
|
self.cs(1)
|
||||||
|
self.dc(0)
|
||||||
|
self.cs(0)
|
||||||
|
self.spi.write(bytearray([cmd]))
|
||||||
|
self.cs(1)
|
||||||
|
|
||||||
|
def write_data(self, buf):
|
||||||
|
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
|
||||||
|
self.cs(1)
|
||||||
|
self.dc(1)
|
||||||
|
self.cs(0)
|
||||||
|
self.spi.write(buf)
|
||||||
|
self.cs(1)
|
221
code/main.py
Normal file
221
code/main.py
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
from waterlevel_sensor import WaterLevelSensor
|
||||||
|
import bme280
|
||||||
|
import sh1106
|
||||||
|
from homie.constants import BOOLEAN, FALSE, TRUE, FLOAT
|
||||||
|
from homie.property import HomieProperty
|
||||||
|
from homie.node import HomieNode
|
||||||
|
from homie.device import HomieDevice, await_ready_state
|
||||||
|
from machine import Pin, ADC, I2C
|
||||||
|
import settings
|
||||||
|
|
||||||
|
import uasyncio as asyncio
|
||||||
|
from time import ticks_ms, ticks_add, ticks_diff
|
||||||
|
|
||||||
|
import gc
|
||||||
|
|
||||||
|
class PlantNode(HomieNode):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name="Planze",
|
||||||
|
pin_watering=Pin(13, Pin.OUT, value=0),
|
||||||
|
pin_moisture=Pin(34),
|
||||||
|
pin_water_tank=Pin(23),
|
||||||
|
i2c=I2C(scl=Pin(18), sda=Pin(19)),
|
||||||
|
interval=60*5,
|
||||||
|
interval_watering=0.1):
|
||||||
|
super().__init__(id="plant", name=name, type="watering")
|
||||||
|
self.i2c = i2c
|
||||||
|
self.display = sh1106.SH1106_I2C(
|
||||||
|
i2c=i2c, width=128, height=64)
|
||||||
|
|
||||||
|
# Update Interval
|
||||||
|
self.interval = interval
|
||||||
|
self.interval_watering = interval_watering
|
||||||
|
self.interval_changed = False
|
||||||
|
self.property_interval = HomieProperty(
|
||||||
|
id="update_interval",
|
||||||
|
name="Aktualisierungsrate",
|
||||||
|
datatype=FLOAT, # TODO ISO8601
|
||||||
|
settable=True,
|
||||||
|
on_message=self._set_interval,
|
||||||
|
unit="s",
|
||||||
|
)
|
||||||
|
self.add_property(self.property_interval)
|
||||||
|
|
||||||
|
# WaterLevelSensor
|
||||||
|
self.waterlevel_sensor = WaterLevelSensor()
|
||||||
|
self.property_waterlevel = HomieProperty(
|
||||||
|
id="waterlevel",
|
||||||
|
name="Wassertankstand",
|
||||||
|
datatype=FLOAT,
|
||||||
|
unit="L",
|
||||||
|
)
|
||||||
|
self.add_property(self.property_waterlevel)
|
||||||
|
self.property_waterlevel_percent = HomieProperty(
|
||||||
|
id="waterlevel_percent",
|
||||||
|
name="Wassertankstand [%]",
|
||||||
|
datatype=FLOAT,
|
||||||
|
format="0.00:100.00",
|
||||||
|
unit="%",
|
||||||
|
)
|
||||||
|
self.add_property(self.property_waterlevel_percent)
|
||||||
|
# self.property_waterlevel_max_value = HomieProperty(
|
||||||
|
# id="waterlevel_max",
|
||||||
|
# name="Wassertankstandsensor Max-Wert",
|
||||||
|
# settable=True,
|
||||||
|
# datatype=FLOAT,
|
||||||
|
# default=1,
|
||||||
|
# unit="#",
|
||||||
|
# on_message=self._set_waterlevel_max_value
|
||||||
|
# )
|
||||||
|
# self.add_property(self.property_waterlevel_max_value)
|
||||||
|
# self.property_waterlevel_min_value = HomieProperty(
|
||||||
|
# id="waterlevel_min",
|
||||||
|
# name="Wassertankstandsensor Min-Wert",
|
||||||
|
# settable=True,
|
||||||
|
# datatype=FLOAT,
|
||||||
|
# default=0,
|
||||||
|
# unit="#",
|
||||||
|
# on_message=self._set_waterlevel_min_value
|
||||||
|
# )
|
||||||
|
# self.add_property(self.property_waterlevel_min_value)
|
||||||
|
self.property_waterlevel_volume_liter = HomieProperty(
|
||||||
|
id="waterlevel_volume_max",
|
||||||
|
name="Wassertankgröße",
|
||||||
|
settable=True,
|
||||||
|
datatype=FLOAT,
|
||||||
|
unit="L",
|
||||||
|
on_message=self._set_waterlevel_volume
|
||||||
|
)
|
||||||
|
self.add_property(self.property_waterlevel_volume_liter)
|
||||||
|
|
||||||
|
# Moisture
|
||||||
|
self.adc = ADC(pin_moisture)
|
||||||
|
self.adc.atten(ADC.ATTN_11DB)
|
||||||
|
self.property_moisture = HomieProperty(
|
||||||
|
id="moisture",
|
||||||
|
name="Feuchte",
|
||||||
|
datatype=FLOAT,
|
||||||
|
format="0.00:100.00",
|
||||||
|
unit="%",
|
||||||
|
)
|
||||||
|
self.add_property(self.property_moisture)
|
||||||
|
|
||||||
|
# BMP280
|
||||||
|
self.bmp280 = bme280.BME280(i2c=self.i2c)
|
||||||
|
self.property_temerature = HomieProperty(
|
||||||
|
id="temperature",
|
||||||
|
name="Temperatur",
|
||||||
|
datatype=FLOAT,
|
||||||
|
unit="°C",
|
||||||
|
)
|
||||||
|
self.add_property(self.property_temerature)
|
||||||
|
self.property_pressure = HomieProperty(
|
||||||
|
id="pressure",
|
||||||
|
name="Druck",
|
||||||
|
datatype=FLOAT,
|
||||||
|
unit="Pa",
|
||||||
|
)
|
||||||
|
self.add_property(self.property_pressure)
|
||||||
|
|
||||||
|
# Watering Motor
|
||||||
|
self.pin_watering_motor = pin_watering
|
||||||
|
self.pin_watering_motor.init(mode=Pin.OUT, value=0)
|
||||||
|
self.property_water_power = HomieProperty(
|
||||||
|
id="power",
|
||||||
|
name="Bewässerung",
|
||||||
|
settable=True,
|
||||||
|
datatype=BOOLEAN,
|
||||||
|
default=FALSE,
|
||||||
|
on_message=self.toggle_motor,
|
||||||
|
)
|
||||||
|
self.add_property(self.property_water_power)
|
||||||
|
|
||||||
|
self.property_watering_max_duration = HomieProperty(
|
||||||
|
id="watering_duration_max",
|
||||||
|
name="Bewässerungszeit",
|
||||||
|
settable=True,
|
||||||
|
datatype=FLOAT,
|
||||||
|
default=3,
|
||||||
|
unit="s",
|
||||||
|
)
|
||||||
|
self.add_property(self.property_watering_max_duration)
|
||||||
|
|
||||||
|
asyncio.create_task(self.update_data())
|
||||||
|
|
||||||
|
@await_ready_state
|
||||||
|
async def update_data(self):
|
||||||
|
while True:
|
||||||
|
self.property_moisture.value = "{:1.2f}".format(
|
||||||
|
(4096 - self.adc.read()) / 40.96)
|
||||||
|
|
||||||
|
self.property_waterlevel_percent.value = "{:1.0f}".format(
|
||||||
|
self.waterlevel_sensor.level_percent)
|
||||||
|
self.property_waterlevel.value = "{:1.2f}".format(
|
||||||
|
self.waterlevel_sensor.level)
|
||||||
|
self.property_waterlevel_volume_liter.value = "{:1.4f}".format(
|
||||||
|
self.waterlevel_sensor.volume)
|
||||||
|
|
||||||
|
self.property_pressure.value = "{:1.0f}".format(
|
||||||
|
self.bmp280.pressure * 100) # hPa = 100 Pa
|
||||||
|
self.property_temerature.value = "{:1.2f}".format(
|
||||||
|
self.bmp280.temperature)
|
||||||
|
|
||||||
|
# TODO ISO8601 self.property_interval.value = "PT{:1.3f}S".format(self.interval)
|
||||||
|
self.property_interval.value = "{:1.3f}".format(self.interval)
|
||||||
|
|
||||||
|
# We don't simply wait the update interval, as it can change while waiting.
|
||||||
|
last_update = ticks_ms()
|
||||||
|
wait_till = ticks_add(last_update, int(self.interval * 1000.0))
|
||||||
|
while ticks_diff(ticks_ms(), wait_till) < 0:
|
||||||
|
watering = self.pin_watering_motor.value() == 1
|
||||||
|
if watering or self.interval_changed:
|
||||||
|
self.interval_changed = False
|
||||||
|
wait_till = ticks_add(
|
||||||
|
last_update,
|
||||||
|
int(self.interval if not watering else self.interval_watering) * 1000)
|
||||||
|
sleep_for = min(int(self.interval_watering * 1000.0),
|
||||||
|
ticks_diff(wait_till, ticks_ms()))
|
||||||
|
await asyncio.sleep_ms(sleep_for)
|
||||||
|
|
||||||
|
def toggle_motor(self, topic, payload, retained):
|
||||||
|
ONOFF = {FALSE: 0, TRUE: 1}
|
||||||
|
v = ONOFF[payload]
|
||||||
|
self.pin_watering_motor(v)
|
||||||
|
if v == 1:
|
||||||
|
asyncio.create_task(self.stop_motor())
|
||||||
|
|
||||||
|
def stop_motor(self):
|
||||||
|
await asyncio.sleep_ms(
|
||||||
|
int(float(self.property_watering_max_duration.value) * 1000))
|
||||||
|
self.pin_watering_motor.value(0)
|
||||||
|
self.property_water_power.value = FALSE
|
||||||
|
|
||||||
|
def _set_waterlevel_min_value(self, topic, payload, retained):
|
||||||
|
self.waterlevel_sensor.value_min = float(payload)
|
||||||
|
|
||||||
|
def _set_waterlevel_max_value(self, topic, payload, retained):
|
||||||
|
self.waterlevel_sensor.value_max = float(payload)
|
||||||
|
|
||||||
|
def _set_waterlevel_volume(self, topic, payload, retained):
|
||||||
|
self.waterlevel_sensor.volume = float(payload)
|
||||||
|
|
||||||
|
def _set_interval(self, topic, payload, retained):
|
||||||
|
self.interval = float(payload)
|
||||||
|
self.interval_changed = True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global plantNode
|
||||||
|
# Homie device setup
|
||||||
|
plantNode = PlantNode()
|
||||||
|
homie = HomieDevice(settings)
|
||||||
|
homie.add_node(plantNode)
|
||||||
|
|
||||||
|
# run forever
|
||||||
|
homie.run_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
83
code/settings.py
Normal file
83
code/settings.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# Debug mode disables WDT, print mqtt messages
|
||||||
|
# DEBUG = False
|
||||||
|
|
||||||
|
###
|
||||||
|
# Wifi settings
|
||||||
|
###
|
||||||
|
|
||||||
|
|
||||||
|
# Multiple WiFi credentials
|
||||||
|
# If a ssid near your device matchs a wifi credentials in the dictionary,
|
||||||
|
# WIFI_SSID and WIFI_PASSWORD will be overwitten with the corresponding
|
||||||
|
# ssid,password. Set to False to disable multible wifis and use WIFI_SSID and
|
||||||
|
# WIFI_PASSWORD to access a WiFi nearby.
|
||||||
|
WIFI_CREDENTIALS = {}
|
||||||
|
with open("wifi-credentials", 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
for i in range(0, len(lines), 2):
|
||||||
|
WIFI_CREDENTIALS[lines[i].replace("\n", "")] = lines[i+1].replace("\n", "")
|
||||||
|
|
||||||
|
WIFI_SSID, WIFI_PASSWORD = list(WIFI_CREDENTIALS.items())[0]
|
||||||
|
# WIFI_CREDENTIALS = None
|
||||||
|
# print(WIFI_CREDENTIALS)
|
||||||
|
|
||||||
|
###
|
||||||
|
# MQTT settings
|
||||||
|
###
|
||||||
|
#
|
||||||
|
|
||||||
|
# Broker IP or DNS Name
|
||||||
|
MQTT_BROKER = "mqtt.nils-server"
|
||||||
|
|
||||||
|
# Broker port
|
||||||
|
MQTT_PORT = 1883
|
||||||
|
|
||||||
|
# Username or None for anonymous login
|
||||||
|
# MQTT_USERNAME = None
|
||||||
|
|
||||||
|
# Password or None for anonymous login
|
||||||
|
# MQTT_PASSWORD = None
|
||||||
|
|
||||||
|
# Defines the mqtt connection timemout in seconds
|
||||||
|
# MQTT_KEEPALIVE = 30
|
||||||
|
|
||||||
|
# SSL connection to the broker. Some MicroPython implementations currently
|
||||||
|
# have problems with receiving mqtt messages over ssl connections.
|
||||||
|
# MQTT_SSL = False
|
||||||
|
# MQTT_SSL_PARAMS = {}
|
||||||
|
# MQTT_SSL_PARAMS = {"do_handshake": True}
|
||||||
|
|
||||||
|
# Base mqtt topic the device publish and subscribes to, without leading slash.
|
||||||
|
# Base topic format is bytestring.
|
||||||
|
# MQTT_BASE_TOPIC = "homie"
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# Device settings
|
||||||
|
###
|
||||||
|
|
||||||
|
# The device ID for registration at the broker. The device id is also the
|
||||||
|
# base topic of a device and must be unique and bytestring.
|
||||||
|
# from homie.utils import get_unique_id
|
||||||
|
DEVICE_ID = "pflanzen-geraet1" # get_unique_id()
|
||||||
|
|
||||||
|
# Friendly name of the device as bytestring
|
||||||
|
DEVICE_NAME = "Pflanzen Gießer"
|
||||||
|
|
||||||
|
# Time in seconds the device updates device properties
|
||||||
|
DEVICE_STATS_INTERVAL = 60
|
||||||
|
|
||||||
|
# Subscribe to broadcast topic is enabled by default. To disable broadcast
|
||||||
|
# messages set BROADCAST to False
|
||||||
|
# BROADCAST = True
|
||||||
|
|
||||||
|
# Enable build-in extensions
|
||||||
|
from homie.constants import EXT_MPY
|
||||||
|
EXTENSIONS = [EXT_MPY]
|
||||||
|
|
||||||
|
# from homie.constants import EXT_MPY, EXT_FW, EXT_STATS
|
||||||
|
# EXTENSIONS = [
|
||||||
|
# EXT_MPY,
|
||||||
|
# EXT_FW,
|
||||||
|
# EXT_STATS,
|
||||||
|
# ]
|
98
code/waterlevel_sensor.py
Normal file
98
code/waterlevel_sensor.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
###
|
||||||
|
# GND ADC VCC
|
||||||
|
# | | |
|
||||||
|
# R1 | |
|
||||||
|
# | | |
|
||||||
|
# |______| |
|
||||||
|
# | |
|
||||||
|
# R2_1 |
|
||||||
|
# | |
|
||||||
|
# R2_2 |
|
||||||
|
# | |
|
||||||
|
# ... ...
|
||||||
|
# | |
|
||||||
|
# ~~~~~WATER~~~~~~~~~
|
||||||
|
# | - current - -|
|
||||||
|
# R2_n |
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# VCC gets toggeled by 'power_pin', so current doesn't flow continously.
|
||||||
|
#
|
||||||
|
# To have equal distances between the voltages following calculation can be used:
|
||||||
|
# r2 = [r1/(1-i/n) - r1/(1-(i-1)/n) for i in range(1,n)]
|
||||||
|
#
|
||||||
|
# example for
|
||||||
|
# n = 4 # 4 r2 resistors
|
||||||
|
# r1 = 10000 # 10k Ohms Resistor to GND
|
||||||
|
# gives resistance values:
|
||||||
|
# -> r2_1 := 3333.3 (~3300 Ohm)
|
||||||
|
# -> r2_2 := 6666.7 (~6800 Ohm)
|
||||||
|
# -> r2_3 := 20000.0
|
||||||
|
#
|
||||||
|
###
|
||||||
|
|
||||||
|
from machine import Pin, ADC
|
||||||
|
# import uasyncio as asyncio
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class WaterLevelSensor:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
power_pin=Pin(26),
|
||||||
|
adc_pin=Pin(35),
|
||||||
|
resistor_ground=10000,
|
||||||
|
resistors_water=[3300, 6800, 20000],
|
||||||
|
resistance_water_tolerance=[400, 550, 700, 850],
|
||||||
|
volume_liter=11*9*8.5/1000):
|
||||||
|
|
||||||
|
self.power_pin = power_pin
|
||||||
|
self.power_pin.init(mode=Pin.OUT, value=0)
|
||||||
|
|
||||||
|
self.adc_pin = adc_pin
|
||||||
|
self.adc = ADC(self.adc_pin)
|
||||||
|
self.adc.atten(ADC.ATTN_11DB)
|
||||||
|
|
||||||
|
self._r1 = resistor_ground
|
||||||
|
self._r2 = resistors_water
|
||||||
|
self.resistance_water_tolerance = resistance_water_tolerance
|
||||||
|
self._voltage_levels = \
|
||||||
|
[4096 * self._r1 / (self._r1 + sum(self._r2[:i]))
|
||||||
|
for i in range(len(self._r2), 0, -1)]
|
||||||
|
print(self._voltage_levels)
|
||||||
|
|
||||||
|
self.min_measure_time_ms = 500
|
||||||
|
self._last_measurment_time_ms = time.ticks_diff(time.ticks_ms(), 500)
|
||||||
|
self._last_level_rel = 0
|
||||||
|
|
||||||
|
self.volume = volume_liter
|
||||||
|
# asyncio.create_task(self._polling())
|
||||||
|
# from waterlevel_sensor import WaterLevelSensor; wls = WaterLevelSensor()
|
||||||
|
|
||||||
|
def _measure_value(self):
|
||||||
|
self.power_pin.on()
|
||||||
|
self._last_measure = self.adc.read()
|
||||||
|
self.power_pin.off()
|
||||||
|
return self._last_measure
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _level_rel(self):
|
||||||
|
now = time.ticks_ms()
|
||||||
|
if time.ticks_diff(now, self._last_measurment_time_ms) \
|
||||||
|
> self.min_measure_time_ms:
|
||||||
|
self._last_measurment_time_ms = now
|
||||||
|
adc_value = self._measure_value()
|
||||||
|
for i, voltage in enumerate(self._voltage_levels):
|
||||||
|
if adc_value >= voltage - self.resistance_water_tolerance[i]:
|
||||||
|
self._last_level_rel = (i / (len(self._voltage_levels)))
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return self._last_level_rel
|
||||||
|
|
||||||
|
@property
|
||||||
|
def level(self):
|
||||||
|
return self._level_rel * self.volume
|
||||||
|
|
||||||
|
@property
|
||||||
|
def level_percent(self):
|
||||||
|
return self._level_rel * 100
|
Loading…
x
Reference in New Issue
Block a user