24 Commits

Author SHA1 Message Date
Jerry
e9c0b13c37 1.0.25 2025-04-07 11:45:31 +08:00
Jerry
cdbca3ee29 1.0.25 2025-04-07 11:37:52 +08:00
Jerry
b301ac575b 1.0.24 2025-04-06 18:01:59 +08:00
Jerry
0d11f01750 1.0.23 2025-04-06 17:19:38 +08:00
Jerry
25101e659a 1.0.23 2025-04-06 16:53:09 +08:00
JADE Solution
8eab03f808 Merge pull request #9 from reiyawea/patch-1
Update holiday.cpp
2025-03-09 13:25:57 +08:00
reiyawea
7057ef3d5b Update holiday.cpp
ArduinoJson 7.3 开始,MemberProxy 和 ElementProxy 变更为non-copyable,所以doc["code"]必须先显式转换到int再做判断。
2025-03-08 18:37:26 +08:00
Jerry
689e4a08c9 1.0.22 2025-02-25 17:52:00 +08:00
Jerry
2aab05ba64 1.0.21 2025-02-24 14:20:31 +08:00
Jerry
9985e5a2b2 1.0.20 2025-02-23 18:13:27 +08:00
Jerry
d5ef1d4a5a 1.0.20 2025-02-23 17:54:26 +08:00
Jerry
2ed10fba8e 1.0.19 2025-02-23 16:58:48 +08:00
Jerry
d951c78711 1.0.19 2025-02-23 16:49:27 +08:00
Jerry
f6a319244c 1.0.18 2025-02-21 10:22:21 +08:00
Jerry
fd097ea497 1.0.17 2025-02-08 18:40:28 +08:00
Jerry
7aa08b878a 1.0.17 2025-02-08 16:56:53 +08:00
Jerry
1e0110757f 1.0.16 2025-01-09 16:55:50 +08:00
Jerry
83f6a9a018 1.0.16 2025-01-09 16:55:27 +08:00
Jerry
8d7c07b739 1.0.16 2025-01-09 09:38:59 +08:00
Jerry
74cec82994 1.0.16 2025-01-08 23:42:27 +08:00
Jerry
3905fa8463 1.0.15 2025-01-08 18:27:16 +08:00
Jerry
ac38b64d96 1.0.15 2025-01-08 18:05:08 +08:00
Jerry
9f73b08d75 1.0.14 2025-01-05 11:44:43 +08:00
Jerry
d8932d56ba 1.0.14 2025-01-05 11:42:28 +08:00
39 changed files with 4002 additions and 25 deletions

7
.gitignore vendored
View File

@@ -1,10 +1,3 @@
.pio
.vscode
/bin
/include/*
/lib/*
/src/*
/test/*
platformio.ini
extra_script.py
.DS_Store

674
LICENSE Normal file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://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 <http://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
<http://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
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@@ -2,6 +2,7 @@
墨水屏日历采用三色4.2寸墨水屏,展示基本月历信息,支持农历、公共假期、倒计日、天气(实时天气、每日天气)展示。<br>
项目以低难度、低成本的方式,方便爱好者实现属于自己的低功耗月历。<br>
<img src="./assets/img/sample.jpg" width="60%"><br>
Bilibili连接https://www.bilibili.com/video/BV1wHDhYoE3G/<br>
注:固件仅供个人下载免费使用,禁止商用。
## Prepare & Meterial
@@ -36,6 +37,7 @@
3. 选择连接的串口以及波特率(波特率可以根据实际情况调整)
4. 擦除Flash。
5. 开始烧录。<br>
<span style="color:red">文件前面一定要打勾否则不会刷进flash的</span><br>
(参考下图)<br>
<img src="./assets/img/flash_download_tool_guide.png" width="70%">
5. 安装盒3D打印用PLA或ABS均可。[E-ink box2 v22.3mf](./assets/file/E-ink%20box2%20v22.3mf)
@@ -44,7 +46,7 @@
1. **单点**
如果处在休眠中,唤醒系统,并强制刷新月历。
如果处在运行中,强制刷新日历。
如果处在配置中,不做任何处理
如果处在配置中,不做任何处理
2. **双击**
系统正常运行中,双击进入系统配置。(并强制停止WIFI相关操作,如获取天气。)
系统配置状态中,双击重启系统。
@@ -53,10 +55,11 @@
## LED Indicator:
(板载LED,PIN-22)
1. 快闪: 系统启动中(正在连接WIFI)
2. 常亮: WIFI连接完成(成功或失败)
3. 三短闪一长灭: 系统配置中
4. 熄灭: 系统休眠
1. 快闪: LED每秒闪约两次系统启动中(正在连接WIFI)
2. 常亮: WIFI连接完成
3. 慢闪: LED每两秒闪一次WIFI连接失败10秒钟后休眠
4. 三短闪一长灭: 系统配置中。3分钟后超时休眠
5. 熄灭: 系统休眠。
## Web Config Guide:
通过在开机状态下(LED常亮)双击,即可进入配置状态,这时系统会生成一个名为J-Calendar的ap,默认密码为:password。(默认超时时间为180秒)
@@ -69,11 +72,14 @@
* 0: 周日(默认); 1: 周一;
* 和风天气:
* 输入和风天气的API Key和城市id(城市对应的id请在和风天气的官网查找。)系统会每2小时刷新当前天气,如果Key置空,天气将不会被刷新。<br>
* 天气类型0:每日天气(默认,每天凌晨刷新一次); 1: 实时天气(每两个小时刷新一次天气)<br>[城市id列表](./assets/file/China-City-List-latest.csv) <br>
* 天气类型0:每日天气(默认,每天凌晨刷新一次); 1: 实时天气(每两个小时刷新一次天气)<br>
* 位置城市id或者经纬度 例1: 101010100 例2: 116.41,39.92<br>
[城市id列表](./assets/file/China-City-List-latest.csv) <br>
* 倒数日:<br>输入倒数日名称和日期,名称不能超过4个中文字符,时间以yyyyMMdd的格式填入。配置正确的话,日历每天会显示倒数“距****还有**天”。如果倒数日名称为空,系统将不显示倒数日信息。
* 日期Tag:<br>
1. 输入格式yyyyMMddxyyyy为年每年显示设为0000MM为月份每月显示设为00dd为日期x为tag的图标a:书签b金钱c笑脸d警告。例如00000015b每年每月15日旁边显示$符号00000312a每年3月12日显示书签符号。
2. 最多可以设置三个tag中间以分号隔开。例如00000015b;00000312a
* 保存配置后,系统自动重启。
3. Update. OTA升级
此项需要在浏览器内完成,通过ip地址访问配置页面,然后进入Update,选择固件文件后上传,等待。刷新完成后,页面会有成功提示。
4. Restart. 重启
@@ -84,19 +90,58 @@
退出配置状态。
## Q & A
1. Q: 可以支持哪种屏?
1. Q: 刷完机后,如何配置?启动流程是如何的?<br>
A: 需要在系统运行状态下状态灯常亮或慢闪时双击按键即可进入配置状态。LED灯变成三短闪一长灭时说明进入配置状态了。
2. Q: 刷完机后,没有反应该如何处理?<br>
A: 观察22针脚的LED是否点亮有闪烁说明固件已经刷入如果没有闪烁可以尝试点击重置按钮重启或者拔掉电源线和usb线重插来重启。此外还可使用串口工具通过usb的com口获取启动日志来进一步确认故障原因。
3. Q: 可以支持哪种屏?<br>
A: 仅4.2寸三色屏(目前仅支持黑白红,将来支持黑白黄)。
2. Q: 我使用的屏应该烧录哪个固件?<br>
A: 从经验上预估SES的拆机屏选z21比较新一些的选z98如果是非常老的屏选z15.不行的话,三个固件都刷一遍试试。<br>
明确的丝印清单如下:
|丝印|固件|
|-----|-----|
|E042A43-A0|z98|
|P420010|z98|
|A13600**|z21|
4. Q: 我使用的屏应该烧录哪个固件?<br>
A: 从经验上预估SES的拆机屏选z21比较新一些的选z98如果是非常老的屏选z15。实在不行的话,三个固件都刷一遍试试。<br>
明确的丝印对应如下:<br>
|丝印|固件|
|-----|-----|
|E042A43-A0|z98|
|P420010|z98|
|A13600**|z21|
5. Q: 使用flash download tool刷新固件时报错连接串口失败。<br>
A: 1. 检查USB线连接是否正常。 <br>2. app的串口下拉框里是否检测到COM口。 <br>3. 把其他串口工具关闭防止占用COM口。
6. Q: 刷新固件的过程中提示报错。<br>
A: 1. 换根质量较好的或短一些的USB线或换个USB口插入。 <br>2. 可以将刷新的波特率降低一点如选择速率低一点的115200。
7. Q: 刷新固件后,需要重新配置吗?
A: 仅刷新app固件后配置是保留的所以无需重新配置。如果刷新了分区表partition.bin会将esp的nvs区刷新这时候需要重新配置。
8. Q: 填入和风天气的API Key和位置ID后没有成功获取天气信息。
A: 3月1日后注册的和风天气账户有API Host限制请下载1.0.25以后版本在配置时配置API Host信息。
## Releases
### 1.0.25 (未测试)
* Fix: 和风天气api变更需要输入API Host在配置页面增加配置项
### 1.0.24
* Refine: 配置页面location约束改为64长度允许输入经纬度设置天气位置e.g. 116.41,39.92
### 1.0.23
* Refine: 修改和风天气请求url。
* Fix: bug(获取holiday返回码)。
### 1.0.22
* 项目开源此项目使用GNU General Public License v3.0许可证授权。详情请参阅LICENSE文件。
* Refine农历计算功能移出。农历功能作为独立库以MIT协议开源
### 1.0.21
* Fix: bug (wrong background color of calender header).
### 1.0.20
* Fix: bugs.
### 1.0.19
* New: 假日信息通过网络API获取。
* Refine: 由于esp32内置时钟的误差会有可能唤醒的时间不准确同时由于处于午夜0点左右导致提前刷新或在刷新期间切换日期导致日期显示不准确。处理方式计时器唤醒的情况下23:50以后不刷新直接休眠等待至0点以后唤醒刷新。
### 1.0.18
* Fix: 修正Wifi连接失败后同步时间导致系统时间错误。
### 1.0.17
* Refine: 如果wifi连接失败等待10s再休眠并增加LED慢闪作为指示。在这时间段内供用户进行双击打开配置状态
* Fix: 修正倒数日名称显示不足的问题。
### 1.0.16
* New Feature: UI调整新增今日农历日期的展现。
### 1.0.15
* Fix: 编译异常。
### 1.0.14
* Fix: bug(假日颜色不正确), 增加显示假日和调休icon(日历右上角)。
### 1.0.13
* Fix: bugs.
### 1.0.12
@@ -128,11 +173,17 @@ A: 从经验上预估SES的拆机屏选z21比较新一些的选z98
* 基本功能
* 功耗优化等
## License
此项目使用GNU General Public License v3.0许可证授权。详情请参阅LICENSE文件。
### ★★★固件还不错,支持一下★★★
<img src="./assets/img/buymeacoffee.jpg" width="32.9%">
<img src="./assets/img/likeit.jpg" width="30%"><br>
## Reference:
1. \<WEMOS LOLIN32簡介\> https://swf.com.tw/?p=1331&cpage=1
2. \<GxEPD2\> https://github.com/ZinggJM/GxEPD2
3. \<U8g2_for_Adafruit_GFX\> https://github.com/olikraus/U8g2_for_Adafruit_GFX
4. \<和风天气\> https://dev.qweather.com/docs/api/weather/weather-now/
<br>
Copyright © 2023-2024. All Rights Reserved.
Copyright © 2023-2025. All Rights Reserved.

BIN
assets/img/buymeacoffee.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
assets/img/likeit.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 417 KiB

43
bin/build.sh Executable file
View File

@@ -0,0 +1,43 @@
export PATH=/Users/jerry/.platformio/penv/bin:/Users/jerry/.platformio/penv:/Users/jerry/.platformio/python3/bin:$PATH
# function
cd ..
basedir=$PWD
### compile ###
cd $basedir
if [ $# -ne 1 ]; then
echo "ERR: parameter [VERSION] is required."
echo "Usage: ./build.sh [VERSION]"
echo " e.g. ./build.sh 1.0.5"
exit 1
fi
version=$1
echo $version
rm -f ./dist/*
pio run -t clean
export PLATFORMIO_BUILD_FLAGS=-DJ_VERSION=\\\"${version}\\\"
pio run -e z98 -e z21 -e z15
if [ $? -ne 0 ]; then
echo "ERR: build err when firmware of Z98."
exit 1
else
cp $basedir/.pio/build/z98/bootloader.bin $basedir/dist/bootloader.bin
cp $basedir/.pio/build/z98/partitions.bin $basedir/dist/partitions.bin
cp $basedir/.pio/build/z98/firmware.bin $basedir/dist/jcalendar_${version}_z98.bin
cp $basedir/.pio/build/z15/firmware.bin $basedir/dist/jcalendar_${version}_z15.bin
cp $basedir/.pio/build/z21/firmware.bin $basedir/dist/jcalendar_${version}_z21.bin
fi
echo
echo
echo
echo "::: Build finish. :::"
echo " Please commit & push to Github."

23
extra_script.py Normal file
View File

@@ -0,0 +1,23 @@
Import("env")
import os
def merge_bin(source, target, env):
firmware_path = os.path.join(env.subst("$BUILD_DIR"), "firmware.bin")
bootloader_path = os.path.join(env.subst("$BUILD_DIR"), "bootloader.bin")
partitions_path = os.path.join(env.subst("$BUILD_DIR"), "partitions.bin")
filesystem_path = os.path.join(env.subst("$BUILD_DIR"), "littlefs.bin")
output_path = os.path.join(env.subst("$PROJECT_DIR"), "/dist/firmware.bin")
cmd = [
"esptool.py",
"--chip", "esp32",
"merge_bin",
"-o", output_path,
"0x1000", bootloader_path,
"0x8000", partitions_path,
"0x10000", firmware_path,
"0x3D0000", filesystem_path
]
env.Execute(" ".join(cmd))
env.AddPostAction("buildprog", merge_bin)

View File

@@ -0,0 +1,339 @@
// Display Library example for SPI e-paper panels from Dalian Good Display and boards from Waveshare.
// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines!
//
// Display Library based on Demo Example from Good Display: https://www.good-display.com/companyfile/32/
//
// Author: Jean-Marc Zingg
//
// Version: see library.properties
//
// Library: https://github.com/ZinggJM/GxEPD2
// Supporting Arduino Forum Topics (closed, read only):
// Good Display ePaper for Arduino: https://forum.arduino.cc/t/good-display-epaper-for-arduino/419657
// Waveshare e-paper displays with SPI: https://forum.arduino.cc/t/waveshare-e-paper-displays-with-spi/467865
//
// Add new topics in https://forum.arduino.cc/c/using-arduino/displays/23 for new questions and issues
// NOTE: you may need to adapt or select for your wiring in the processor specific conditional compile sections below
// select the display class (only one), matching the kind of display panel
//#define GxEPD2_DISPLAY_CLASS GxEPD2_BW
#define GxEPD2_DISPLAY_CLASS GxEPD2_3C
//#define GxEPD2_DISPLAY_CLASS GxEPD2_4C
//#define GxEPD2_DISPLAY_CLASS GxEPD2_7C
// 脚本编译需要,根据命令行参数编译不同版本固件。
#if defined(SI_DRIVER)
#if (SI_DRIVER == 15)
#define GxEPD2_DRIVER_CLASS GxEPD2_420c
#elif (SI_DRIVER == 21)
#define GxEPD2_DRIVER_CLASS GxEPD2_420c_Z21
#elif (SI_DRIVER == 98)
#define GxEPD2_DRIVER_CLASS GxEPD2_420c_GDEY042Z98
#endif
#endif
// 默认固件
#ifndef GxEPD2_DRIVER_CLASS
// #define GxEPD2_DRIVER_CLASS GxEPD2_420c // Z15
// #define GxEPD2_DRIVER_CLASS GxEPD2_420c_Z21 // Z21
#define GxEPD2_DRIVER_CLASS GxEPD2_420c_GDEY042Z98 // Z98
#endif
// select the display driver class (only one) for your panel
//#define GxEPD2_DRIVER_CLASS GxEPD2_102 // GDEW0102T4 80x128, UC8175, (WFT0102CZA2)
//#define GxEPD2_DRIVER_CLASS GxEPD2_150_BN // DEPG0150BN 200x200, SSD1681, (FPC8101), TTGO T5 V2.4.1
//#define GxEPD2_DRIVER_CLASS GxEPD2_154 // GDEP015OC1 200x200, IL3829, (WFC0000CZ07), no longer available
//#define GxEPD2_DRIVER_CLASS GxEPD2_154_D67 // GDEH0154D67 200x200, SSD1681, (HINK-E154A07-A1)
//#define GxEPD2_DRIVER_CLASS GxEPD2_154_T8 // GDEW0154T8 152x152, UC8151 (IL0373), (WFT0154CZ17)
//#define GxEPD2_DRIVER_CLASS GxEPD2_154_M09 // GDEW0154M09 200x200, JD79653A, (WFT0154CZB3)
//#define GxEPD2_DRIVER_CLASS GxEPD2_154_M10 // GDEW0154M10 152x152, UC8151D, (WFT0154CZ17)
//#define GxEPD2_DRIVER_CLASS GxEPD2_154_GDEY0154D67 // GDEY0154D67 200x200, SSD1681, (FPC-B001 20.05.21)
//#define GxEPD2_DRIVER_CLASS GxEPD2_213 // GDE0213B1 122x250, IL3895, (HINK-E0213-G01), phased out
//#define GxEPD2_DRIVER_CLASS GxEPD2_213_B72 // GDEH0213B72 122x250, SSD1675A (IL3897), (HINK-E0213A22-A0 SLH1852)
//#define GxEPD2_DRIVER_CLASS GxEPD2_213_B73 // GDEH0213B73 122x250, SSD1675B, (HINK-E0213A22-A0 SLH1914)
//#define GxEPD2_DRIVER_CLASS GxEPD2_213_B74 // GDEM0213B74 122x250, SSD1680, FPC-7528B)
//#define GxEPD2_DRIVER_CLASS GxEPD2_213_flex // GDEW0213I5F 104x212, UC8151 (IL0373), (WFT0213CZ16)
//#define GxEPD2_DRIVER_CLASS GxEPD2_213_M21 // GDEW0213M21 104x212, UC8151 (IL0373), (WFT0213CZ16)
//#define GxEPD2_DRIVER_CLASS GxEPD2_213_T5D // GDEW0213T5D 104x212, UC8151D, (WFT0213CZ16)
//#define GxEPD2_DRIVER_CLASS GxEPD2_213_BN // DEPG0213BN 122x250, SSD1680, (FPC-7528B), TTGO T5 V2.4.1, V2.3.1
//#define GxEPD2_DRIVER_CLASS GxEPD2_213_GDEY0213B74 // GDEY0213B74 122x250, SSD1680, (FPC-A002 20.04.08)
//#define GxEPD2_DRIVER_CLASS GxEPD2_260 // GDEW026T0 152x296, UC8151 (IL0373), (WFT0154CZ17)
//#define GxEPD2_DRIVER_CLASS GxEPD2_260_M01 // GDEW026M01 152x296, UC8151 (IL0373), (WFT0260CZB2)
//#define GxEPD2_DRIVER_CLASS GxEPD2_266_BN // DEPG0266BN 152x296, SSD1680, (FPC7510), TTGO T5 V2.66, TTGO T5 V2.4.1
//#define GxEPD2_DRIVER_CLASS GxEPD2_266_GDEY0266T90 // GDEY0266T90 152x296, SSD1680, (FPC-A003 HB)
//#define GxEPD2_DRIVER_CLASS GxEPD2_270 // GDEW027W3 176x264, EK79652 (IL91874), (WFI0190CZ22)
//#define GxEPD2_DRIVER_CLASS GxEPD2_270_GDEY027T91 // GDEY027T91 176x264, SSD1680, (FB)
//#define GxEPD2_DRIVER_CLASS GxEPD2_290 // GDEH029A1 128x296, SSD1608 (IL3820), (E029A01-FPC-A1 SYX1553)
//#define GxEPD2_DRIVER_CLASS GxEPD2_290_T5 // GDEW029T5 128x296, UC8151 (IL0373), (WFT0290CZ10)
//#define GxEPD2_DRIVER_CLASS GxEPD2_290_T5D // GDEW029T5D 128x296, UC8151D, (WFT0290CZ10)
//#define GxEPD2_DRIVER_CLASS GxEPD2_290_I6FD // GDEW029I6FD 128x296, UC8151D, (WFT0290CZ10)
//#define GxEPD2_DRIVER_CLASS GxEPD2_290_T94 // GDEM029T94 128x296, SSD1680, (FPC-7519 rev.b)
//#define GxEPD2_DRIVER_CLASS GxEPD2_290_T94_V2 // GDEM029T94 128x296, SSD1680, (FPC-7519 rev.b), Waveshare 2.9" V2 variant
//#define GxEPD2_DRIVER_CLASS GxEPD2_290_BS // DEPG0290BS 128x296, SSD1680, (FPC-7519 rev.b)
//#define GxEPD2_DRIVER_CLASS GxEPD2_290_M06 // GDEW029M06 128x296, UC8151D, (WFT0290CZ10)
//#define GxEPD2_DRIVER_CLASS GxEPD2_290_GDEY029T94 // GDEY029T94 128x296, SSD1680, (FPC-A005 20.06.15)
//#define GxEPD2_DRIVER_CLASS GxEPD2_290_GDEY029T71H // GDEY029T71H 168x384, SSD1685, (FPC-H004 22.03.24)
//#define GxEPD2_DRIVER_CLASS GxEPD2_310_GDEQ031T10 // GDEQ031T10 240x320, UC8253, (no inking, backside mark KEGMO 3100)
//#define GxEPD2_DRIVER_CLASS GxEPD2_371 // GDEW0371W7 240x416, UC8171 (IL0324), (missing)
//#define GxEPD2_DRIVER_CLASS GxEPD2_370_TC1 // ED037TC1 280x480, SSD1677, (ICA-FU-20 ichia 2029), Waveshare 3.7"
//#define GxEPD2_DRIVER_CLASS GxEPD2_420 // GDEW042T2 400x300, UC8176 (IL0398), (WFT042CZ15)
//#define GxEPD2_DRIVER_CLASS GxEPD2_420_M01 // GDEW042M01 400x300, UC8176 (IL0398), (WFT042CZ15)
//#define GxEPD2_DRIVER_CLASS GxEPD2_420_GDEY042T81 // GDEY042T81 400x300, SSD1683 (no inking)
//#define GxEPD2_DRIVER_CLASS GxEPD2_420_GYE042A87 // GYE042A87, 400x300, SSD1683 (HINK-E042-A07-FPC-A1)
//#define GxEPD2_DRIVER_CLASS GxEPD2_420_SE0420NQ04 // SE0420NQ04, 400x300, UC8276C (OPM042A2_V1.0)
//#define GxEPD2_DRIVER_CLASS GxEPD2_426_GDEQ0426T82 // GDEQ0426T82 480x800, SSD1677 (P426010-MF1-A)
//#define GxEPD2_DRIVER_CLASS GxEPD2_579_GDEY0579T93 // GDEY0579T93 792x272, SSD1683 (FPC-E004 22.04.13)
//#define GxEPD2_DRIVER_CLASS GxEPD2_583 // GDEW0583T7 600x448, UC8159c (IL0371), (missing)
//#define GxEPD2_DRIVER_CLASS GxEPD2_583_T8 // GDEW0583T8 648x480, EK79655 (GD7965), (WFT0583CZ61)
//#define GxEPD2_DRIVER_CLASS GxEPD2_583_GDEQ0583T31 // GDEQ0583T31 648x480, UC8179, (P583010-MF1-B)
//#define GxEPD2_DRIVER_CLASS GxEPD2_750 // GDEW075T8 640x384, UC8159c (IL0371), (WF0583CZ09)
//#define GxEPD2_DRIVER_CLASS GxEPD2_750_T7 // GDEW075T7 800x480, EK79655 (GD7965), (WFT0583CZ61)
//#define GxEPD2_DRIVER_CLASS GxEPD2_750_GDEY075T7 // GDEY075T7 800x480, UC8179 (GD7965), (FPC-C001 20.08.20)
//#define GxEPD2_DRIVER_CLASS GxEPD2_1020_GDEM102T91 // GDEM102T91 960x640, SSD1677, (FPC7705 REV.b)
//#define GxEPD2_DRIVER_CLASS GxEPD2_1085_GDEM1085T51 // GDEM1085T51 1360x480, JD79686AB, (FPC8617) *** needs CS2 ***
//#define GxEPD2_DRIVER_CLASS GxEPD2_1160_T91 // GDEH116T91 960x640, SSD1677, (none or hidden)
//#define GxEPD2_DRIVER_CLASS GxEPD2_1248 // GDEW1248T3 1304x984, UC8179, (WFT1248BZ23,WFT1248BZ24)
//#define GxEPD2_DRIVER_CLASS GxEPD2_1330_GDEM133T91 // GDEM133T91 960x680, SSD1677, (FPC-7701 REV.B)
// 3-color e-papers
//#define GxEPD2_DRIVER_CLASS GxEPD2_154c // GDEW0154Z04 200x200, IL0376F, (WFT0000CZ04), no longer available
//#define GxEPD2_DRIVER_CLASS GxEPD2_154_Z90c // GDEH0154Z90 200x200, SSD1681, (HINK-E154A07-A1)
//#define GxEPD2_DRIVER_CLASS GxEPD2_213c // GDEW0213Z16 104x212, UC8151 (IL0373), (WFT0213CZ16)
//#define GxEPD2_DRIVER_CLASS GxEPD2_213_Z19c // GDEH0213Z19 104x212, UC8151D, (HINK-E0213A20-A2 2020-11-19)
//#define GxEPD2_DRIVER_CLASS GxEPD2_213_Z98c // GDEY0213Z98 122x250, SSD1680, (FPC-A002 20.04.08)
//#define GxEPD2_DRIVER_CLASS GxEPD2_266c // GDEY0266Z90 152x296, SSD1680, (FPC-7510)
//#define GxEPD2_DRIVER_CLASS GxEPD2_270c // GDEW027C44 176x264, IL91874, (WFI0190CZ22)
//#define GxEPD2_DRIVER_CLASS GxEPD2_290c // GDEW029Z10 128x296, UC8151 (IL0373), (WFT0290CZ10)
//#define GxEPD2_DRIVER_CLASS GxEPD2_290_Z13c // GDEH029Z13 128x296, UC8151D, (HINK-E029A10-A3 20160809)
//#define GxEPD2_DRIVER_CLASS GxEPD2_290_C90c // GDEM029C90 128x296, SSD1680, (FPC-7519 rev.b)
//#define GxEPD2_DRIVER_CLASS GxEPD2_420c // GDEW042Z15 400x300, UC8176 (IL0398), (WFT0420CZ15)
//#define GxEPD2_DRIVER_CLASS GxEPD2_420c_Z21 // GDEQ042Z21 400x300, UC8276, (hidden)
//#define GxEPD2_DRIVER_CLASS GxEPD2_420c_GDEY042Z98 // GDEY042Z98 400x300, SSD1683 (no inking)
//#define GxEPD2_DRIVER_CLASS GxEPD2_579c_GDEY0579Z93 // GDEY0579Z93 792x272, SSD1683 (FPC-E004 22.04.13)
//#define GxEPD2_DRIVER_CLASS GxEPD2_583c // GDEW0583Z21 600x448, UC8159c (IL0371), (missing)
//#define GxEPD2_DRIVER_CLASS GxEPD2_583c_Z83 // GDEW0583Z83 648x480, EK79655 (GD7965), (WFT0583CZ61)
//#define GxEPD2_DRIVER_CLASS GxEPD2_583c_GDEQ0583Z31 // GDEQ0583Z31 648x480, UC8179C,
//#define GxEPD2_DRIVER_CLASS GxEPD2_750c // GDEW075Z09 640x384, UC8159c (IL0371), (WF0583CZ09)
//#define GxEPD2_DRIVER_CLASS GxEPD2_750c_Z08 // GDEW075Z08 800x480, EK79655 (GD7965), (WFT0583CZ61)
//#define GxEPD2_DRIVER_CLASS GxEPD2_750c_Z90 // GDEH075Z90 880x528, SSD1677, (HINK-E075A07-A0)
//#define GxEPD2_DRIVER_CLASS GxEPD2_1160c_GDEY116Z91 // GDEY116Z91 960x640, SSD1677, (FPV-K002 22.04.15)
//#define GxEPD2_DRIVER_CLASS GxEPD2_1248c // GDEY1248Z51 1304x984, UC8179, (WFT1248BZ23,WFT1248BZ24)
//#define GxEPD2_DRIVER_CLASS GxEPD2_1330c_GDEM133Z91 // GDEM133Z91 960x680, SSD1677 (FPC-7701 REV.B)
// 4-color e-paper
//#define GxEPD2_DRIVER_CLASS GxEPD2_213c_GDEY0213F51 // GDEY0213F51 122x250, JD79661 (FPC-A002 20.04.08)
//#define GxEPD2_DRIVER_CLASS GxEPD2_266c_GDEY0266F51H // GDEY0266F51H 184x360, JD79667 (FPC-H006 22.04.02)
//#define GxEPD2_DRIVER_CLASS GxEPD2_290c_GDEY029F51H // GDEY029F51H 168x384, JD79667 (FPC-H004 22.03.24)
//#define GxEPD2_DRIVER_CLASS GxEPD2_300c // Waveshare 3.00" 4-color
//#define GxEPD2_DRIVER_CLASS GxEPD2_420c_GDEY0420F51 // GDEY0420F51 400x300, HX8717 (no inking)
//#define GxEPD2_DRIVER_CLASS GxEPD2_437c // Waveshare 4.37" 4-color
//#define GxEPD2_DRIVER_CLASS GxEPD2_0579c_GDEY0579F51 // GDEY0579F51 792x272, HX8717 (FPC-E009 22.09.25)
//#define GxEPD2_DRIVER_CLASS GxEPD2_1160c_GDEY116F51 // GDEY116F51 960x640, SSD2677, (FPC-K012 23.09.27)
// 7-color e-paper
//#define GxEPD2_DRIVER_CLASS GxEPD2_565c // Waveshare 5.65" 7-color
//#define GxEPD2_DRIVER_CLASS GxEPD2_565c_GDEP0565D90 // GDEP0565D90 600x448 7-color (E219454, AB1024-EGA AC0750TC1)
//#define GxEPD2_DRIVER_CLASS GxEPD2_730c_GDEY073D46 // GDEY073D46 800x480 7-color, (N-FPC-001 2021.11.26)
//#define GxEPD2_DRIVER_CLASS GxEPD2_730c_ACeP_730 // Waveshare 7.3" 7-color
//#define GxEPD2_DRIVER_CLASS GxEPD2_730c_GDEP073E01 // GDEP073E01 800x480 7-color, (E350911HF 94V-0 F-6 ROHS 24141)
// grey levels parallel IF e-papers on Waveshare e-Paper IT8951 Driver HAT
//#define GxEPD2_DRIVER_CLASS GxEPD2_it60 // ED060SCT 800x600
//#define GxEPD2_DRIVER_CLASS GxEPD2_it60_1448x1072 // ED060KC1 1448x1072
//#define GxEPD2_DRIVER_CLASS GxEPD2_it78_1872x1404 // ED078KC2 1872x1404
//#define GxEPD2_DRIVER_CLASS GxEPD2_it103_1872x1404 // ES103TC1 1872x1404
// SS is usually used for CS. define here for easy change
#ifndef EPD_CS
#define EPD_CS SS
#endif
#if defined(GxEPD2_DISPLAY_CLASS) && defined(GxEPD2_DRIVER_CLASS)
// somehow there should be an easier way to do this
#define GxEPD2_BW_IS_GxEPD2_BW true
#define GxEPD2_3C_IS_GxEPD2_3C true
#define GxEPD2_4C_IS_GxEPD2_4C true
#define GxEPD2_7C_IS_GxEPD2_7C true
#define GxEPD2_1248_IS_GxEPD2_1248 true
#define GxEPD2_1248c_IS_GxEPD2_1248c true
#define IS_GxEPD(c, x) (c##x)
#define IS_GxEPD2_BW(x) IS_GxEPD(GxEPD2_BW_IS_, x)
#define IS_GxEPD2_3C(x) IS_GxEPD(GxEPD2_3C_IS_, x)
#define IS_GxEPD2_4C(x) IS_GxEPD(GxEPD2_4C_IS_, x)
#define IS_GxEPD2_7C(x) IS_GxEPD(GxEPD2_7C_IS_, x)
#define IS_GxEPD2_1248(x) IS_GxEPD(GxEPD2_1248_IS_, x)
#define IS_GxEPD2_1248c(x) IS_GxEPD(GxEPD2_1248c_IS_, x)
#include "GxEPD2_selection_check.h"
#if defined (ESP8266)
#define MAX_DISPLAY_BUFFER_SIZE (81920ul-34000ul-5000ul) // ~34000 base use, change 5000 to your application use
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
#elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8))
#elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
// adapt the constructor parameters to your wiring
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=D8*/ EPD_CS, /*DC=D3*/ 0, /*RST=D4*/ 2, /*BUSY=D2*/ 4));
// mapping of Waveshare e-Paper ESP8266 Driver Board, new version
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=15*/ EPD_CS, /*DC=4*/ 4, /*RST=2*/ 2, /*BUSY=5*/ 5));
// mapping of Waveshare e-Paper ESP8266 Driver Board, old version
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=15*/ EPD_CS, /*DC=4*/ 4, /*RST=5*/ 5, /*BUSY=16*/ 16));
#undef MAX_DISPLAY_BUFFER_SIZE
#undef MAX_HEIGHT
#endif
#if defined(ESP32)
#define MAX_DISPLAY_BUFFER_SIZE 65536ul // e.g.
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
#elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8))
#elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
// adapt the constructor parameters to your wiring
#if !IS_GxEPD2_1248(GxEPD2_DRIVER_CLASS) && !IS_GxEPD2_1248c(GxEPD2_DRIVER_CLASS)
#if defined(ARDUINO_LOLIN_D32_PRO)
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=5*/ EPD_CS, /*DC=*/ 0, /*RST=*/ 2, /*BUSY=*/ 15)); // my LOLIN_D32_PRO proto board
#elif defined(ARDUINO_LOLIN_S2_MINI)
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS*/ 33, /*DC=*/ 35, /*RST=*/ 37, /*BUSY=*/ 39)); // my LOLIN ESP32 S2 mini connection
#else
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ 27, /*DC=*/ 14, /*RST=*/ 12, /*BUSY=*/ 13)); // Good Display ESP32 Development Kit ESP32-L
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ 27, /*DC=*/ 14, /*RST=*/ 12, /*BUSY=*/ 13, /*CS2=*/ 4)); // for GDEM1085T51 with ESP32-L
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=5*/ EPD_CS, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // my suggested wiring and proto board
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=5*/ 5, /*DC=*/ 17, /*RST=*/ 16, /*BUSY=*/ 4)); // LILYGO_T5_V2.4.1
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=5*/ EPD_CS, /*DC=*/ 19, /*RST=*/ 4, /*BUSY=*/ 34)); // LILYGO® TTGO T5 2.66
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=5*/ EPD_CS, /*DC=*/ 2, /*RST=*/ 0, /*BUSY=*/ 4)); // e.g. TTGO T8 ESP32-WROVER
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ 15, /*DC=*/ 27, /*RST=*/ 26, /*BUSY=*/ 25)); // Waveshare ESP32 Driver Board
#endif
#else // GxEPD2_1248 or GxEPD2_1248c
// Waveshare 12.48 b/w or b/w/r SPI display board and frame or Good Display 12.48 b/w panel GDEW1248T3 or b/w/r panel GDEY1248Z51
// general constructor for use with all parameters, e.g. for Waveshare ESP32 driver board mounted on connection board
GxEPD2_DISPLAY_CLASS < GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS) > display(GxEPD2_DRIVER_CLASS(/*sck=*/ 13, /*miso=*/ 12, /*mosi=*/ 14,
/*cs_m1=*/ 23, /*cs_s1=*/ 22, /*cs_m2=*/ 16, /*cs_s2=*/ 19,
/*dc1=*/ 25, /*dc2=*/ 17, /*rst1=*/ 33, /*rst2=*/ 5,
/*busy_m1=*/ 32, /*busy_s1=*/ 26, /*busy_m2=*/ 18, /*busy_s2=*/ 4));
#endif
#undef MAX_DISPLAY_BUFFER_SIZE
#undef MAX_HEIGHT
#endif
// can't use package "STMF1 Boards (STM32Duino.com)" (Roger Clark) anymore with Adafruit_GFX, use "STM32 Boards (selected from submenu)" (STMicroelectronics)
#if defined(ARDUINO_ARCH_STM32)
#define MAX_DISPLAY_BUFFER_SIZE 15000ul // ~15k is a good compromise
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
#elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8))
#elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
// adapt the constructor parameters to your wiring
// for Good Display STM32 Development Kit DESPI-L.
// needs jumpers from PA5 (PIN_SPI_SCK) to SCK for EPD and PA7 (PIN_SPI_MOSI) to SDI for EPD. PD9 and PD10 are not HW SPI capable.
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ PD8, /*DC=*/ PE15, /*RST=*/ PE14, /*BUSY=*/ PE13));
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ PD8, /*DC=*/ PE15, /*RST=*/ PE14, /*BUSY=*/ PE13, /*CS2=*/ PD12)); // for GDEM1085T51
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=PA4*/ EPD_CS, /*DC=*/ PA3, /*RST=*/ PA2, /*BUSY=*/ PA1)); // my proto board
#undef MAX_DISPLAY_BUFFER_SIZE
#undef MAX_HEIGHT
#endif
#if defined(__AVR)
#if defined (ARDUINO_AVR_MEGA2560) // Note: SS is on 53 on MEGA
#define MAX_DISPLAY_BUFFER_SIZE 5000 // e.g. full height for 200x200
#else // Note: SS is on 10 on UNO, NANO
#define MAX_DISPLAY_BUFFER_SIZE 800 //
#endif
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
#elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8))
#elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
// adapt the constructor parameters to your wiring
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ EPD_CS, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7));
// for Arduino Micro or Arduino Leonardo with CS on 10 on my proto boards (SS would be 17) uncomment instead:
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ 10, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7));
#endif
#if defined(ARDUINO_UNOR4_MINIMA) || defined(ARDUINO_UNOR4_WIFI)
#define MAX_DISPLAY_BUFFER_SIZE 16384ul // e.g. half of available RAM
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
#elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8))
#elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
// adapt the constructor parameters to your wiring
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ EPD_CS, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7));
#endif
#if defined(ARDUINO_ARCH_SAM)
#define MAX_DISPLAY_BUFFER_SIZE 32768ul // e.g., up to 96k
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
#elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8))
#elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
// adapt the constructor parameters to your wiring
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=10*/ EPD_CS, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7));
#undef MAX_DISPLAY_BUFFER_SIZE
#undef MAX_HEIGHT
#endif
#if defined(ARDUINO_ARCH_SAMD)
#define MAX_DISPLAY_BUFFER_SIZE 15000ul // ~15k is a good compromise
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
#elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8))
#elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
// adapt the constructor parameters to your wiring
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=4*/ 4, /*DC=*/ 7, /*RST=*/ 6, /*BUSY=*/ 5));
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=4*/ 4, /*DC=*/ 3, /*RST=*/ 2, /*BUSY=*/ 1)); // my Seed XIOA0
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=4*/ 3, /*DC=*/ 2, /*RST=*/ 1, /*BUSY=*/ 0)); // my other Seed XIOA0
#undef MAX_DISPLAY_BUFFER_SIZE
#undef MAX_HEIGHT
#endif
#if defined(ARDUINO_ARCH_RP2040)
#define MAX_DISPLAY_BUFFER_SIZE 131072ul // e.g. half of available ram
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
#elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) || IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8))
#elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS)
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
#endif
#if defined(ARDUINO_NANO_RP2040_CONNECT)
// adapt the constructor parameters to your wiring
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ EPD_CS, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7));
#endif
#if defined(ARDUINO_RASPBERRY_PI_PICO)
// adapt the constructor parameters to your wiring
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ 5, /*DC=*/ 8, /*RST=*/ 9, /*BUSY=*/ 7)); // my proto board
// mapping of GoodDisplay DESPI-PICO. NOTE: uses alternate HW SPI pins!
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ 3, /*DC=*/ 2, /*RST=*/ 1, /*BUSY=*/ 0)); // DESPI-PICO
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ 3, /*DC=*/ 2, /*RST=*/ 11, /*BUSY=*/ 10)); // DESPI-PICO modified
//GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ 9, /*DC=*/ 8, /*RST=*/ 12, /*BUSY=*/ 13)); // Waveshare Pico-ePaper-2.9
#endif
#if defined(ARDUINO_RASPBERRY_PI_PICO_W)
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ 9, /*DC=*/ 8, /*RST=*/ 12, /*BUSY=*/ 13)); // Waveshare Pico-ePaper-2.9
#endif
#if defined(ARDUINO_ADAFRUIT_FEATHER_RP2040_THINKINK)
// Adafruit Feather RP2040 ThinkInk used with package https://github.com/earlephilhower/arduino-pico
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ PIN_EPD_CS, /*DC=*/ PIN_EPD_DC, /*RST=*/ PIN_EPD_RESET, /*BUSY=*/ PIN_EPD_BUSY));
#endif
#undef MAX_DISPLAY_BUFFER_SIZE
#undef MAX_HEIGHT
#endif
#endif

View File

@@ -0,0 +1,156 @@
// Display Library example for SPI e-paper panels from Dalian Good Display and boards from Waveshare.
// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines!
//
// Display Library based on Demo Example from Good Display: https://www.good-display.com/companyfile/32/
//
// Author: Jean-Marc Zingg
//
// Version: see library.properties
//
// Library: https://github.com/ZinggJM/GxEPD2
// Supporting Arduino Forum Topics (closed, read only):
// Good Display ePaper for Arduino: https://forum.arduino.cc/t/good-display-epaper-for-arduino/419657
// Waveshare e-paper displays with SPI: https://forum.arduino.cc/t/waveshare-e-paper-displays-with-spi/467865
//
// Add new topics in https://forum.arduino.cc/c/using-arduino/displays/23 for new questions and issues
#define GxEPD2_102_IS_BW true
#define GxEPD2_150_BN_IS_BW true
#define GxEPD2_154_IS_BW true
#define GxEPD2_154_D67_IS_BW true
#define GxEPD2_154_T8_IS_BW true
#define GxEPD2_154_M09_IS_BW true
#define GxEPD2_154_M10_IS_BW true
#define GxEPD2_154_GDEY0154D67_IS_BW true
#define GxEPD2_213_IS_BW true
#define GxEPD2_213_B72_IS_BW true
#define GxEPD2_213_B73_IS_BW true
#define GxEPD2_213_B74_IS_BW true
#define GxEPD2_213_flex_IS_BW true
#define GxEPD2_213_M21_IS_BW true
#define GxEPD2_213_T5D_IS_BW true
#define GxEPD2_213_BN_IS_BW true
#define GxEPD2_213_GDEY0213B74_IS_BW true
#define GxEPD2_260_IS_BW true
#define GxEPD2_260_M01_IS_BW true
#define GxEPD2_266_BN_IS_BW true
#define GxEPD2_266_GDEY0266T90_IS_BW true
#define GxEPD2_270_IS_BW true
#define GxEPD2_270_GDEY027T91_IS_BW true
#define GxEPD2_290_IS_BW true
#define GxEPD2_290_T5_IS_BW true
#define GxEPD2_290_T5D_IS_BW true
#define GxEPD2_290_I6FD_IS_BW true
#define GxEPD2_290_T94_IS_BW true
#define GxEPD2_290_T94_V2_IS_BW true
#define GxEPD2_290_BS_IS_BW true
#define GxEPD2_290_M06_IS_BW true
#define GxEPD2_290_GDEY029T94_IS_BW true
#define GxEPD2_290_GDEY029T71H_IS_BW true
#define GxEPD2_310_GDEQ031T10_IS_BW true
#define GxEPD2_371_IS_BW true
#define GxEPD2_370_TC1_IS_BW true
#define GxEPD2_420_IS_BW true
#define GxEPD2_420_M01_IS_BW true
#define GxEPD2_420_GDEY042T81_IS_BW true
#define GxEPD2_420_GYE042A87_IS_BW true
#define GxEPD2_420_SE0420NQ04_IS_BW true
#define GxEPD2_426_GDEQ0426T82_IS_BW true
#define GxEPD2_579_GDEY0579T93_IS_BW true
#define GxEPD2_583_IS_BW true
#define GxEPD2_583_T8_IS_BW true
#define GxEPD2_583_GDEQ0583T31_IS_BW true
#define GxEPD2_750_IS_BW true
#define GxEPD2_750_T7_IS_BW true
#define GxEPD2_750_GDEY075T7_IS_BW true
#define GxEPD2_1020_GDEM102T91_IS_BW true
#define GxEPD2_1085_GDEM1085T51_IS_BW true
#define GxEPD2_1160_T91_IS_BW true
#define GxEPD2_1248_IS_BW true
#define GxEPD2_1330_GDEM133T91_IS_BW true
#define GxEPD2_it60_IS_BW true
#define GxEPD2_it60_1448x1072_IS_BW true
#define GxEPD2_it78_1872x1404_IS_BW true
#define GxEPD2_it103_1872x1404_IS_BW true
// 3-color e-papers
#define GxEPD2_154c_IS_3C true
#define GxEPD2_154_Z90c_IS_3C true
#define GxEPD2_213c_IS_3C true
#define GxEPD2_213_Z19c_IS_3C true
#define GxEPD2_213_Z98c_IS_3C true
#define GxEPD2_266c_IS_3C true
#define GxEPD2_270c_IS_3C true
#define GxEPD2_290c_IS_3C true
#define GxEPD2_290_Z13c_IS_3C true
#define GxEPD2_290_C90c_IS_3C true
#define GxEPD2_420c_IS_3C true
#define GxEPD2_420c_Z21_IS_3C true
#define GxEPD2_420c_GDEY042Z98_IS_3C true
#define GxEPD2_579c_GDEY0579Z93_IS_3C true
#define GxEPD2_583c_IS_3C true
#define GxEPD2_583c_Z83_IS_3C true
#define GxEPD2_583c_GDEQ0583Z31_IS_3C true
#define GxEPD2_750c_IS_3C true
#define GxEPD2_750c_Z08_IS_3C true
#define GxEPD2_750c_Z90_IS_3C true
#define GxEPD2_1160c_GDEY116Z91_IS_3C true
#define GxEPD2_1248c_IS_3C true
#define GxEPD2_1330c_GDEM133Z91_IS_3C true
// 4-color e-paper
#define GxEPD2_213c_GDEY0213F51_IS_4C true
#define GxEPD2_266c_GDEY0266F51H_IS_4C true
#define GxEPD2_290c_GDEY029F51H_IS_4C true
#define GxEPD2_300c_IS_4C true
#define GxEPD2_420c_GDEY0420F51_IS_4C true
#define GxEPD2_437c_IS_4C true
#define GxEPD2_0579c_GDEY0579F51_IS_4C true
#define GxEPD2_1160c_GDEY116F51_IS_4C true
// 7-color e-paper
#define GxEPD2_565c_IS_7C true
#define GxEPD2_565c_GDEP0565D90_IS_7C true
#define GxEPD2_730c_GDEY073D46_IS_7C true
#define GxEPD2_730c_ACeP_730_IS_7C true
#define GxEPD2_730c_GDEP073E01_IS_7C true
#if defined(GxEPD2_DISPLAY_CLASS) && defined(GxEPD2_DRIVER_CLASS)
#define IS_GxEPD2_DRIVER(c, x) (c##x)
#define IS_GxEPD2_DRIVER_BW(x) IS_GxEPD2_DRIVER(x, _IS_BW)
#define IS_GxEPD2_DRIVER_3C(x) IS_GxEPD2_DRIVER(x, _IS_3C)
#define IS_GxEPD2_DRIVER_4C(x) IS_GxEPD2_DRIVER(x, _IS_4C)
#define IS_GxEPD2_DRIVER_7C(x) IS_GxEPD2_DRIVER(x, _IS_7C)
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_3C(GxEPD2_DRIVER_CLASS)
#error "GxEPD2_BW used with 3-color driver class"
#endif
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_4C(GxEPD2_DRIVER_CLASS)
#error "GxEPD2_BW used with 4-color driver class"
#endif
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_7C(GxEPD2_DRIVER_CLASS)
#error "GxEPD2_BW used with 7-color driver class"
#endif
#if IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_BW(GxEPD2_DRIVER_CLASS)
#error "GxEPD2_3C used with b/w driver class"
#endif
#if IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_4C(GxEPD2_DRIVER_CLASS)
#error "GxEPD2_3C used with 4-color driver class"
#endif
#if IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_7C(GxEPD2_DRIVER_CLASS)
#error "GxEPD2_3C used with 7-color driver class"
#endif
#if IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_BW(GxEPD2_DRIVER_CLASS)
#error "GxEPD2_4C used with b/w driver class"
#endif
#if IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_3C(GxEPD2_DRIVER_CLASS)
#error "GxEPD2_4C used with 3-color driver class"
#endif
#if IS_GxEPD2_4C(GxEPD2_DISPLAY_CLASS) && IS_GxEPD2_DRIVER_7C(GxEPD2_DRIVER_CLASS)
#error "GxEPD2_4C used with 7-color driver class"
#endif
#if IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS) && !IS_GxEPD2_DRIVER_7C(GxEPD2_DRIVER_CLASS)
#error "GxEPD2_7C used with less colors driver class"
#endif
#if !IS_GxEPD2_DRIVER_BW(GxEPD2_DRIVER_CLASS) && !IS_GxEPD2_DRIVER_3C(GxEPD2_DRIVER_CLASS) && !IS_GxEPD2_DRIVER_4C(GxEPD2_DRIVER_CLASS) && !IS_GxEPD2_DRIVER_7C(GxEPD2_DRIVER_CLASS)
#error "neither BW nor 3C nor 4C nor 7C kind defined for driver class (error in GxEPD2_selection_check.h)"
#endif
#endif

39
include/README Normal file
View File

@@ -0,0 +1,39 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

22
include/_preference.h Normal file
View File

@@ -0,0 +1,22 @@
#ifndef ___PREFERENCE_H__
#define ___PREFERENCE_H__
#include <Preferences.h>
#define PREF_NAMESPACE "J_CALENDAR"
// Preferences KEY定义
// !!!preferences key限制15字符
#define PREF_SI_CAL_DATE "SI_CAL_DATE" // 屏幕当前显示的日期
#define PREF_SI_WEEK_1ST "SI_WEEK_1ST" // 每周第一天0: 周日默认1:周一
#define PREF_QWEATHER_HOST "QWEATHER_HOST" // QWEATHER HOST
#define PREF_QWEATHER_KEY "QWEATHER_KEY" // QWEATHER KEY/TOKEN
#define PREF_QWEATHER_TYPE "QWEATHER_TYPE" // 0: 每日天气1: 实时天气
#define PREF_QWEATHER_LOC "QWEATHER_LOC" // 地理位置
#define PREF_CD_DAY_DATE "CD_DAY_DATE" // 倒计日
#define PREF_CD_DAY_LABLE "CD_DAY_LABLE" // 倒计日名称
#define PREF_TAG_DAYS "TAG_DAYS" // tag day
// 假期信息tm年假期日(int8),假期日(int8)...
#define PREF_HOLIDAY "HOLIDAY"
#endif

15
include/_sntp.h Normal file
View File

@@ -0,0 +1,15 @@
#ifndef ___SNTP_H__
#define ___SNTP_H__
int _sntp_status();
void _sntp_exec(int status = 0);
typedef enum {
SYNC_STATUS_IDLE = -1,
SYNC_STATUS_IN_PROGRESS = 0,
SYNC_STATUS_OK = 1,
SYNC_STATUS_NOK = 2,
SYNC_STATUS_TOO_LATE = 3
} _sntp_status_t;
#endif

9
include/font.h Executable file
View File

@@ -0,0 +1,9 @@
#ifndef __FONT_H__
#define __FONT_H__
#include <stdbool.h>
#include <u8g2_fonts.h>
extern const uint8_t u8g2_font_qweather_icon_16[] U8G2_FONT_SECTION("u8g2_font_qweather_icon_16");
#endif // __FONT_H__

13
include/holiday.h Normal file
View File

@@ -0,0 +1,13 @@
#ifndef ___HOLIDAY_H__
#define ___HOLIDAY_H__
struct Holiday {
int year;
int month;
int holidays[50];
int length;
};
bool getHolidays(Holiday& result, int year, int month);
#endif

15
include/led.h Normal file
View File

@@ -0,0 +1,15 @@
#ifndef ___LED_H__
#define ___LED_H__
#include <Arduino.h>
#define PIN_LED GPIO_NUM_22
void led_init();
void led_fast();
void led_slow();
void led_on();
void led_off();
void led_config();
#endif

12
include/ota.h Normal file
View File

@@ -0,0 +1,12 @@
#define HTTP_UE_TOO_LESS_SPACE (-100)
#define HTTP_UE_SERVER_NOT_REPORT_SIZE (-101)
#define HTTP_UE_SERVER_FILE_NOT_FOUND (-102)
#define HTTP_UE_SERVER_FORBIDDEN (-103)
#define HTTP_UE_SERVER_WRONG_HTTP_CODE (-104)
#define HTTP_UE_SERVER_FAULTY_MD5 (-105)
#define HTTP_UE_BIN_VERIFY_HEADER_FAILED (-106)
#define HTTP_UE_BIN_FOR_WRONG_FLASH (-107)
#define HTTP_UE_NO_PARTITION (-108)
void ota_update();

13
include/screen_ink.h Normal file
View File

@@ -0,0 +1,13 @@
int si_calendar_status();
void si_calendar();
int si_wifi_status();
void si_wifi();
int si_weather_status();
void si_weather();
int si_screen_status();
void si_screen();
void print_status();

4
include/version.h Normal file
View File

@@ -0,0 +1,4 @@
#ifndef J_VERSION
#define J_VERSION "1.0.x-dev"
#endif

13
include/weather.h Normal file
View File

@@ -0,0 +1,13 @@
#ifndef __WEATHER_H__
#define __WEATHER_H__
#include <API.hpp>
int8_t weather_type();
int8_t weather_status();
Weather* weather_data_now();
DailyForecast* weather_data_daily();
void weather_exec(int status = 0);
void weather_stop();
#endif

46
lib/README Normal file
View File

@@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.
The source code of each library should be placed in an own separate directory
("lib/your_library_name/[here are source files]").
For example, see a structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

10
library.properties Normal file
View File

@@ -0,0 +1,10 @@
name=JCalendar
version=1.0.22
author=Jerry <jerry@jade-solution.com>
maintainer=Jerry <jerry@jade-solution.com>
sentence=墨水屏桌面日历
paragraph=桌面小摆件,显示日历及天气信息及附属功能。
architectures=esp32
url=https://github.com/JADE-Jerry/jcalendar
license=GNU General Public License v3.0
category=Display

7
min_spiffs.csv Normal file
View File

@@ -0,0 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1E0000,
app1, app, ota_1, 0x1F0000,0x1E0000,
spiffs, data, spiffs, 0x3D0000,0x20000,
coredump, data, coredump,0x3F0000,0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 app0 app ota_0 0x10000 0x1E0000
5 app1 app ota_1 0x1F0000 0x1E0000
6 spiffs data spiffs 0x3D0000 0x20000
7 coredump data coredump 0x3F0000 0x10000

75
platformio.ini Normal file
View File

@@ -0,0 +1,75 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
description = Ink Screen Calendar
default_envs =
z21
[common]
framework = arduino
lib_deps =
zinggjm/GxEPD2@^1.6.0
olikraus/U8g2_for_Adafruit_GFX@^1.8.0
bblanchon/ArduinoJson@^7.2.0
tzapu/WiFiManager@^2.0.17
mathertel/OneButton@^2.6.1
https://github.com/JADE-Jerry/nongli.git
https://github.com/tignioj/ArduinoUZlib
[env:z98]
build_type = release
platform = espressif32
board = esp32dev
framework = ${common.framework}
lib_deps = ${common.lib_deps}
upload_speed = 460800
monitor_speed = 115200
board_build.partitions = min_spiffs.csv
build_flags =
-D SI_DRIVER=98
[env:z21]
build_type = release
platform = espressif32
board = esp32dev
framework = ${common.framework}
lib_deps = ${common.lib_deps}
upload_speed = 460800
monitor_speed = 115200
board_build.partitions = min_spiffs.csv
build_flags =
-D SI_DRIVER=21
;extra_scripts = post:extra_script.py
[env:z15]
build_type = release
platform = espressif32
board = esp32dev
framework = ${common.framework}
lib_deps = ${common.lib_deps}
upload_speed = 460800
monitor_speed = 115200
board_build.partitions = min_spiffs.csv
build_flags =
-D SI_DRIVER=15
[env:esp32c3]
platform = espressif32
board = esp32-c3-devkitm-1
board_build.flash_mode = dio
framework = ${common.framework}
lib_deps = ${common.lib_deps}
upload_speed = 460800
monitor_speed = 115200
board_build.partitions = min_spiffs.csv
build_flags =
-DARDUINO_USB_MODE=1 ;开启USB Slave 功能
-DARDUINO_USB_CDC_ON_BOOT=1 ;开启CDC 下载功能宏

299
src/API.hpp Executable file
View File

@@ -0,0 +1,299 @@
#ifndef __API_HPP__
#define __API_HPP__
#include <Arduino.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <esp_http_client.h>
#include <ArduinoUZlib.h> // 解压gzip
struct Weather {
String time;
int8_t temp;
int8_t humidity;
int16_t wind360;
String windDir;
int8_t windScale;
uint8_t windSpeed;
uint16_t icon;
String text;
String updateTime;
};
struct DailyWeather {
String date;
String sunrise;
String sunset;
String moonPhase;
uint16_t moonPhaseIcon;
int8_t tempMax;
int8_t tempMin;
int8_t humidity;
uint16_t iconDay;
String textDay;
uint16_t iconNight;
String textNight;
int16_t wind360Day;
String windDirDay;
int8_t windScaleDay;
uint8_t windSpeedDay;
int16_t wind360Night;
String windDirNight;
int8_t windScaleNight;
uint8_t windSpeedNight;
};
struct HourlyForecast {
Weather* weather;
uint8_t length;
uint8_t interval;
};
struct DailyForecast {
DailyWeather* weather;
uint8_t length;
String updateTime;
};
struct Hitokoto {
String sentence;
String from;
String from_who;
};
struct Bilibili {
uint64_t follower;
uint64_t view;
uint64_t likes;
};
template<uint8_t MAX_RETRY = 3>
class API {
using callback = std::function<bool(JsonDocument&)>;
using precall = std::function<void()>;
private:
HTTPClient http;
WiFiClientSecure wifiClient;
bool getRestfulAPI(String url, callback cb, precall pre = precall()) {
// Serial.printf("Request Url: %s\n", url.c_str());
JsonDocument doc;
for (uint8_t i = 0; i < MAX_RETRY; i++) {
bool shouldRetry = false;
if (http.begin(wifiClient, url)) {
if (pre) pre();
// Serial.printf("Before GET\n");
int httpCode = http.GET();
// Serial.printf("GET %d\n", httpCode);
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_NOT_MODIFIED) {
bool isGzip = false;
int headers = http.headers();
for (int j = 0; j < headers; j++) {
String headerName = http.headerName(j);
String headerValue = http.header(j);
// Serial.println(headerName + ": " + headerValue);
if (headerName.equalsIgnoreCase("Content-Encoding") && headerValue.equalsIgnoreCase("gzip")) {
isGzip = true;
break;
}
}
String s = http.getString();
DeserializationError error;
if (isGzip) {
// gzip解压缩
uint8_t* outBuf = NULL;
size_t outLen = 0;
ArduinoUZlib::decompress((uint8_t*)s.c_str(), (uint32_t)s.length(), outBuf, outLen);
error = deserializeJson(doc, (char*)outBuf, outLen);
} else {
error = deserializeJson(doc, s);
}
if (!error) {
wifiClient.flush();
http.end();
return cb(doc);
} else {
Serial.print(F("Parse JSON failed, error: "));
Serial.println(error.c_str());
shouldRetry = error == DeserializationError::IncompleteInput;
}
} else {
Serial.print(F("Get failed, error: "));
if (httpCode < 0) {
Serial.println(http.errorToString(httpCode));
shouldRetry = httpCode == HTTPC_ERROR_CONNECTION_REFUSED || httpCode == HTTPC_ERROR_CONNECTION_LOST || httpCode == HTTPC_ERROR_READ_TIMEOUT;
} else {
Serial.println(httpCode);
}
}
wifiClient.flush();
http.end();
} else {
Serial.println(F("Unable to connect"));
}
if (!shouldRetry) break;
Serial.println(F("Retry after 10 second"));
delay(5000);
}
return false;
}
public:
API() {
// http.setTimeout(10000);
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
const char* encoding = "Content-Encoding";
const char* headerKeys[1] = {};
headerKeys[0] = encoding;
http.collectHeaders(headerKeys, 1);
wifiClient.setInsecure();
}
~API() {}
// 获取 HTTPClient
HTTPClient& httpClient() {
return http;
}
// 和风天气 - 实时天气: https://dev.qweather.com/docs/api/weather/weather-now/
bool getWeatherNow(Weather& result, const char* host, const char* key, const char* locid) {
// return getRestfulAPI("https://www.baidu.com", [&result](JsonDocument& json) {
return getRestfulAPI(
"https://" + String(host) + "/v7/weather/now?key=" + String(key) + "&location=" + String(locid), [&result](JsonDocument& json) {
if (strcmp(json["code"], "200") != 0) {
Serial.print(F("Get weather failed, error: "));
Serial.println(json["code"].as<const char*>());
return false;
}
result.updateTime = String(json["updateTime"].as<const char*>());
JsonObject now = json["now"];
result.time = now["obsTime"].as<const char*>();
result.temp = atoi(now["temp"]);
result.humidity = atoi(now["humidity"]);
result.wind360 = atoi(now["wind360"]);
result.windDir = now["windDir"].as<const char*>();
result.windScale = atoi(now["windScale"]);
result.windSpeed = atoi(now["windSpeed"]);
result.icon = atoi(now["icon"]);
result.text = now["text"].as<const char*>();
return true;
});
}
// 和风天气 - 逐小时天气预报: https://dev.qweather.com/docs/api/weather/weather-hourly-forecast/
bool getForecastHourly(HourlyForecast& result, const char* host, const char* key, const char* locid) {
return getRestfulAPI("https://" + String(host) + "/v7/weather/24h?key=" + String(key) + "&location=" + String(locid), [&result](JsonDocument& json) {
if (strcmp(json["code"], "200") != 0) {
Serial.print(F("Get hourly forecast failed, error: "));
Serial.println(json["code"].as<const char*>());
return false;
}
uint8_t i, hours = json["hourly"].size();
for (i = 0; i < result.length; i++) {
if (i * result.interval >= hours) break;
Weather& weather = result.weather[i];
JsonObject hourly = json["hourly"][i * result.interval];
weather.time = hourly["fxTime"].as<const char*>();
weather.temp = atoi(hourly["temp"]);
weather.humidity = atoi(hourly["humidity"]);
weather.wind360 = atoi(hourly["wind360"]);
weather.windDir = hourly["windDir"].as<const char*>();
weather.windScale = atoi(hourly["windScale"]);
weather.windSpeed = atoi(hourly["windSpeed"]);
weather.icon = atoi(hourly["icon"]);
weather.text = hourly["text"].as<const char*>();
}
result.length = i;
return true;
});
}
// 和风天气 - 逐天天气预报: https://dev.qweather.com/docs/api/weather/weather-daily-forecast/
bool getForecastDaily(DailyForecast& result, const char* host, const char* key, const char* locid) {
return getRestfulAPI("https://" + String(host) + "/v7/weather/3d?key=" + String(key) + "&location=" + String(locid), [&result](JsonDocument& json) {
if (strcmp(json["code"], "200") != 0) {
Serial.print(F("Get daily forecast failed, error: "));
Serial.println(json["code"].as<const char*>());
return false;
}
result.updateTime = String(json["updateTime"].as<const char*>());
uint8_t i;
for (i = 0; i < result.length; i++) {
DailyWeather& weather = result.weather[i];
JsonObject daily = json["daily"][i];
weather.date = daily["fxDate"].as<const char*>();
weather.sunrise = daily["sunrise"].as<const char*>();
weather.sunset = daily["sunset"].as<const char*>();
weather.moonPhase = daily["moonPhase"].as<const char*>();
weather.moonPhaseIcon = atoi(daily["moonPhaseIcon"]);
weather.tempMax = atoi(daily["tempMax"]);
weather.tempMin = atoi(daily["tempMin"]);
weather.humidity = atoi(daily["humidity"]);
weather.iconDay = atoi(daily["iconDay"]);
weather.textDay = daily["textDay"].as<const char*>();
weather.iconNight = atoi(daily["iconNight"]);
weather.textNight = daily["textNight"].as<const char*>();
weather.wind360Day = atoi(daily["wind360Day"]);
weather.windDirDay = daily["windDirDay"].as<const char*>();
weather.windScaleDay = atoi(daily["windScaleDay"]);
weather.windSpeedDay = atoi(daily["windSpeedDay"]);
weather.wind360Night = atoi(daily["wind360Night"]);
weather.windDirNight = daily["windDirNight"].as<const char*>();
weather.windScaleNight = atoi(daily["windScaleNight"]);
weather.windSpeedNight = atoi(daily["windSpeedNight"]);
}
result.length = i;
return true;
});
}
// 一言: https://developer.hitokoto.cn/sentence/
bool getHitokoto(Hitokoto& result) {
return getRestfulAPI("https://v1.hitokoto.cn/?max_length=15", [&result](JsonDocument& json) {
result.sentence = json["hitokoto"].as<const char*>();
result.from = json["from"].as<const char*>();
result.from_who = json["from_who"].as<const char*>();
return true;
});
}
// B站粉丝
bool getFollower(Bilibili& result, uint32_t uid) {
return getRestfulAPI("https://api.bilibili.com/x/relation/stat?vmid=" + String(uid), [&result](JsonDocument& json) {
if (json["code"] != 0) {
Serial.print(F("Get bilibili follower failed, error: "));
Serial.println(json["message"].as<const char*>());
return false;
}
result.follower = json["data"]["follower"];
return true;
});
}
// B站总播放量和点赞数
bool getLikes(Bilibili& result, uint32_t uid, const char* cookie) {
return getRestfulAPI(
"https://api.bilibili.com/x/space/upstat?mid=" + String(uid), [&result](JsonDocument& json) {
if (json["code"] != 0) {
Serial.print(F("Get bilibili likes failed, error: "));
Serial.println(json["message"].as<const char*>());
return false;
}
result.view = json["data"]["archive"]["view"];
result.likes = json["data"]["likes"];
return true;
},
[this, &cookie]() {
http.addHeader("Cookie", String("SESSDATA=") + cookie + ";");
});
}
};
#endif // __API_HPP__

0
src/_preference.cpp Normal file
View File

103
src/_sntp.cpp Normal file
View File

@@ -0,0 +1,103 @@
#include "_sntp.h"
#include <WiFi.h>
#include "esp_sntp.h"
#include "Arduino.h"
#include <_preference.h>
#include <API.hpp>
#include "holiday.h"
TaskHandle_t* _handler;
int _status = SYNC_STATUS_IDLE;
int _sntp_status() {
return _status;
}
/*
void time_sync_notification_cb(struct timeval *tv)
{
sntp_stop();
status = SYNC_STATUS_OK;
if (_handler != NULL)
{
vTaskDelete(_handler);
_handler == NULL;
}
}
*/
// SNTP 校准时间的任务
void _sntp_task(void* pvParameter) {
sntp_setoperatingmode(SNTP_OPMODE_POLL);
sntp_setservername(0, "ntp.aliyun.com");
// sntp_setservername(1, "pool.ntp.org");
// 设置时区
setenv("TZ", "CST-8", 1);
// sntp_set_time_sync_notification_cb(time_sync_notification_cb); // 回调函数
sntp_init();
unsigned long begin_millis = millis();
int c = 1;
int re;
while (1) {
if (sntp_get_sync_status() == SNTP_SYNC_STATUS_COMPLETED) {
Serial.println("SNTP OK.");
re = SYNC_STATUS_OK;
break; // 获取时间成功,退出循环
}
if (millis() - begin_millis > 10 * 1000) { // 超时10s
Serial.println("SNTP timeout.");
re = SYNC_STATUS_NOK;
break;
}
c++;
vTaskDelay(pdMS_TO_TICKS(100));
}
time_t now = time(NULL);
struct tm tmInfo = { 0 };
localtime_r(&now, &tmInfo);
Serial.printf("Now: %d-%02d-%02d %02d:%02d:%02d\r\n", tmInfo.tm_year + 1900, tmInfo.tm_mon + 1, tmInfo.tm_mday, tmInfo.tm_hour, tmInfo.tm_min, tmInfo.tm_sec);
// 如果当前时间是23:50之后并且是定时器唤醒的情况下不作后续处理直接休眠待新的一天唤醒
if(tmInfo.tm_hour == 23 && tmInfo.tm_min > 50 && esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_TIMER) {
re = SYNC_STATUS_TOO_LATE;
} else {
Holiday _holiday;
Preferences pref;
pref.begin(PREF_NAMESPACE);
size_t holiday_size = pref.getBytesLength(PREF_HOLIDAY);
if (holiday_size > 0) {
pref.getBytes(PREF_HOLIDAY, &_holiday, holiday_size);
}
pref.end();
if (_holiday.year != tmInfo.tm_year + 1900 || _holiday.month != tmInfo.tm_mon + 1) {
if (getHolidays(_holiday, tmInfo.tm_year + 1900, tmInfo.tm_mon + 1)) {
pref.begin(PREF_NAMESPACE);
pref.putBytes(PREF_HOLIDAY, &_holiday, sizeof(_holiday));
pref.end();
}
}
}
_status = re;
vTaskDelete(NULL); // 删除任务
}
void _sntp_exec(int status) {
_status = status;
if (_status > 0) {
return;
}
if (!WiFi.isConnected()) {
_status = SYNC_STATUS_NOK;
}
_status = SYNC_STATUS_IN_PROGRESS;
// 创建一个SNTP 校准时间的任务
xTaskCreate(&_sntp_task, "_sntp_task", 1024 * 8, NULL, 6, _handler);
}

62
src/holiday.cpp Normal file
View File

@@ -0,0 +1,62 @@
#include "holiday.h"
#include "Arduino.h"
#include <HTTPClient.h>
#include <ArduinoJson.h>
// 获取当月假期信息。数组中数字为当月日期,正为休假,负为补工作日
// https://timor.tech/api/holiday/
bool getHolidays(Holiday& result, int year, int month) {
String req = "https://timor.tech/api/holiday/year/" + String(year) + "-" + String(month);
HTTPClient http;
http.setTimeout(10 * 1000);
http.begin(req);
Serial.printf("Request: %s\n", req.c_str());
int httpCode = http.GET();
if (httpCode != 200) {
http.end();
Serial.println(HTTPClient::errorToString(httpCode));
Serial.println("假日数据获取失败");
return false;
}
String resp = http.getString();
// Serial.printf("Response: %s\n", resp.c_str());
JsonDocument doc;
DeserializationError error = deserializeJson(doc, resp);
if (error) {
http.end();
Serial.print(F("Parse holiday data failed: "));
Serial.println(error.f_str());
return false;
}
http.end();
int status = doc["code"].as<int>();
if (status != 0) {
Serial.println("Get holidays error.");
return false;
}
result.year = year;
result.month = month;
JsonObject oHoliday = doc["holiday"].as<JsonObject>();
int i = 0;
Serial.printf("Holiday: ");
for (JsonPair kv : oHoliday) {
String key = String(kv.key().c_str());
JsonObject value = kv.value();
bool isHoliday = value["holiday"].as<bool>();
int day = key.substring(3, 5).toInt();
result.holidays[i] = day * (isHoliday ? 1 : -1); // 假期为正,补工作日为负
Serial.printf("%d ", result.holidays[i]);
i++;
if (i >= 50) break;
}
Serial.println();
result.length = i;
return true;
}

110
src/led.cpp Normal file
View File

@@ -0,0 +1,110 @@
#include "led.h"
#define PIN_LED GPIO_NUM_22
TaskHandle_t LED_HANDLER;
int8_t BLINK_TYPE;
void led_init()
{
pinMode(PIN_LED, OUTPUT);
}
void task_led(void *param)
{
while(1)
{
switch(BLINK_TYPE)
{
case 0:
digitalWrite(PIN_LED, HIGH); // Off
vTaskDelay(pdMS_TO_TICKS(1000));
break;
case 1:
digitalWrite(PIN_LED, LOW); // On
vTaskDelay(pdMS_TO_TICKS(1000));
break;
case 2:
digitalWrite(PIN_LED, LOW); // On
vTaskDelay(pdMS_TO_TICKS(1000));
digitalWrite(PIN_LED, HIGH); // Off
vTaskDelay(pdMS_TO_TICKS(1000));
break;
case 3:
digitalWrite(PIN_LED, LOW); // On
vTaskDelay(pdMS_TO_TICKS(200));
digitalWrite(PIN_LED, HIGH); // Off
vTaskDelay(pdMS_TO_TICKS(200));
break;
case 4:
vTaskDelay(pdMS_TO_TICKS(200));
digitalWrite(PIN_LED, LOW); // On
vTaskDelay(pdMS_TO_TICKS(200));
digitalWrite(PIN_LED, HIGH); // Off
vTaskDelay(pdMS_TO_TICKS(200));
digitalWrite(PIN_LED, LOW); // On
vTaskDelay(pdMS_TO_TICKS(200));
digitalWrite(PIN_LED, HIGH); // Off
vTaskDelay(pdMS_TO_TICKS(200));
digitalWrite(PIN_LED, LOW); // On
vTaskDelay(pdMS_TO_TICKS(200));
digitalWrite(PIN_LED, HIGH); // Off
vTaskDelay(pdMS_TO_TICKS(1000));
break;
default:
digitalWrite(PIN_LED, HIGH); // Off
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
}
void led_fast()
{
BLINK_TYPE = 3;
if (LED_HANDLER != NULL)
{
vTaskDelete(LED_HANDLER);
}
xTaskCreate(task_led, "TASK_LED", 2048, NULL, 5, &LED_HANDLER);
}
void led_slow()
{
BLINK_TYPE = 2;
if (LED_HANDLER != NULL)
{
vTaskDelete(LED_HANDLER);
}
xTaskCreate(task_led, "TASK_LED", 2048, NULL, 5, &LED_HANDLER);
}
void led_config()
{
BLINK_TYPE = 4;
if (LED_HANDLER != NULL)
{
vTaskDelete(LED_HANDLER);
}
xTaskCreate(task_led, "TASK_LED", 2048, NULL, 5, &LED_HANDLER);
}
void led_on()
{
BLINK_TYPE = 1;
if (LED_HANDLER != NULL)
{
vTaskDelete(LED_HANDLER);
}
xTaskCreate(task_led, "TASK_LED", 2048, NULL, 5, &LED_HANDLER);
}
void led_off()
{
BLINK_TYPE = 0;
if (LED_HANDLER != NULL)
{
vTaskDelete(LED_HANDLER);
}
xTaskCreate(task_led, "TASK_LED", 2048, NULL, 5, &LED_HANDLER);
}

359
src/main.cpp Executable file
View File

@@ -0,0 +1,359 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#include <WiFiManager.h>
#include "OneButton.h"
#include "led.h"
#include "_sntp.h"
#include "weather.h"
#include "screen_ink.h"
#include "_preference.h"
#include "version.h"
#define PIN_BUTTON GPIO_NUM_14 // 注意由于此按键负责唤醒因此需要选择支持RTC唤醒的PIN脚。
OneButton button(PIN_BUTTON, true);
void IRAM_ATTR checkTicks() {
button.tick();
}
WiFiManager wm;
WiFiManagerParameter para_qweather_host("qweather_host", "和风天气Host", "", 64); // 和风天气key
WiFiManagerParameter para_qweather_key("qweather_key", "和风天气API Key", "", 32); // 和风天气key
// const char* test_html = "<br/><label for='test'>天气模式</label><br/><input type='radio' name='test' value='0' checked> 每日天气test </input><input type='radio' name='test' value='1'> 实时天气test</input>";
// WiFiManagerParameter para_test(test_html);
WiFiManagerParameter para_qweather_type("qweather_type", "天气类型0:每日天气1:实时天气)", "0", 2, "pattern='\\[0-1]{1}'"); // 城市code
WiFiManagerParameter para_qweather_location("qweather_loc", "位置ID", "", 64); // 城市code
WiFiManagerParameter para_cd_day_label("cd_day_label", "倒数日4字以内", "", 10); // 倒数日
WiFiManagerParameter para_cd_day_date("cd_day_date", "日期yyyyMMdd", "", 8, "pattern='\\d{8}'"); // 城市code
WiFiManagerParameter para_tag_days("tag_days", "日期TagyyyyMMddx详见README", "", 30); // 日期Tag
WiFiManagerParameter para_si_week_1st("si_week_1st", "每周起始0:周日1:周一)", "0", 2, "pattern='\\[0-1]{1}'"); // 每周第一天
void print_wakeup_reason() {
esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
switch (wakeup_reason) {
case ESP_SLEEP_WAKEUP_EXT0:
Serial.println("Wakeup caused by external signal using RTC_IO");
break;
case ESP_SLEEP_WAKEUP_EXT1:
Serial.println("Wakeup caused by external signal using RTC_CNTL");
break;
case ESP_SLEEP_WAKEUP_TIMER:
Serial.println("Wakeup caused by timer");
break;
case ESP_SLEEP_WAKEUP_TOUCHPAD:
Serial.println("Wakeup caused by touchpad");
break;
case ESP_SLEEP_WAKEUP_ULP:
Serial.println("Wakeup caused by ULP program");
break;
default:
Serial.printf("Wakeup was not caused by deep sleep.\n");
}
}
void buttonClick(void* oneButton);
void buttonDoubleClick(void* oneButton);
void buttonLongPressStop(void* oneButton);
void go_sleep();
unsigned long _idle_millis;
unsigned long TIME_TO_SLEEP = 180 * 1000;
bool _wifi_flag = false;
unsigned long _wifi_failed_millis;
void setup() {
delay(10);
Serial.begin(115200);
Serial.println(".");
print_wakeup_reason();
Serial.println("\r\n\r\n\r\n");
delay(10);
button.setClickMs(300);
button.setPressMs(3000); // 设置长按的时长
button.attachClick(buttonClick, &button);
button.attachDoubleClick(buttonDoubleClick, &button);
// button.attachMultiClick()
button.attachLongPressStop(buttonLongPressStop, &button);
attachInterrupt(digitalPinToInterrupt(PIN_BUTTON), checkTicks, CHANGE);
Serial.printf("***********************\r\n");
Serial.printf(" J-Calendar\r\n");
Serial.printf(" version: %s\r\n", J_VERSION);
Serial.printf("***********************\r\n\r\n");
Serial.printf("Copyright © 2022-2025 JADE Software Co., Ltd. All Rights Reserved.\r\n\r\n");
led_init();
led_fast();
Serial.println("Wm begin...");
wm.setHostname("J-Calendar");
wm.setEnableConfigPortal(false);
wm.setConnectTimeout(10);
if (wm.autoConnect()) {
Serial.println("Connect OK.");
led_on();
_wifi_flag = true;
} else {
Serial.println("Connect failed.");
_wifi_flag = false;
_wifi_failed_millis = millis();
led_slow();
_sntp_exec(2);
weather_exec(2);
WiFi.mode(WIFI_OFF); // 提前关闭WIFI省电
Serial.println("Wifi closed.");
}
}
/**
* 处理各个任务
* 1. sntp同步
* 前置条件Wifi已连接
* 2. 刷新日历
* 前置条件sntp同步完成无论成功或失败
* 3. 刷新天气信息
* 前置条件wifi已连接
* 4. 系统配置
* 前置条件:无
* 5. 休眠
* 前置条件:所有任务都完成或失败,
*/
void loop() {
button.tick(); // 单击,刷新页面;双击,打开配置;长按,重启
wm.process();
// 前置任务wifi已连接
// sntp同步
if (_sntp_status() == -1) {
_sntp_exec();
}
// 如果是定时器唤醒并且接近午夜23:50之后则直接休眠
if (_sntp_status() == SYNC_STATUS_TOO_LATE) {
go_sleep();
}
// 前置任务wifi已连接
// 获取Weather信息
if (weather_status() == -1) {
weather_exec();
}
// 刷新日历
// 前置任务sntp、weather
// 执行条件:屏幕状态为待处理
if (_sntp_status() > 0 && weather_status() > 0 && si_screen_status() == -1) {
// 数据获取完毕后关闭Wifi省电
if (!wm.getConfigPortalActive()) {
WiFi.mode(WIFI_OFF);
}
Serial.println("Wifi closed after data fetch.");
si_screen();
}
// 休眠
// 前置条件:屏幕刷新完成(或成功)
// 未在配置状态,且屏幕刷新完成,进入休眠
if (!wm.getConfigPortalActive() && si_screen_status() > 0) {
if(_wifi_flag) {
go_sleep();
}
if(!_wifi_flag && millis() - _wifi_failed_millis > 10 * 1000) { // 如果wifi连接不成功等待10秒休眠
go_sleep();
}
}
// 配置状态下,
if (wm.getConfigPortalActive() && millis() - _idle_millis > TIME_TO_SLEEP) {
go_sleep();
}
delay(10);
}
// 刷新页面
void buttonClick(void* oneButton) {
Serial.println("Button click.");
if (wm.getConfigPortalActive()) {
Serial.println("In config status.");
} else {
Serial.println("Refresh screen manually.");
si_screen();
}
}
void saveParamsCallback() {
Preferences pref;
pref.begin(PREF_NAMESPACE);
pref.putString(PREF_QWEATHER_HOST, para_qweather_host.getValue());
pref.putString(PREF_QWEATHER_KEY, para_qweather_key.getValue());
pref.putString(PREF_QWEATHER_TYPE, strcmp(para_qweather_type.getValue(), "1") == 0 ? "1" : "0");
pref.putString(PREF_QWEATHER_LOC, para_qweather_location.getValue());
pref.putString(PREF_CD_DAY_LABLE, para_cd_day_label.getValue());
pref.putString(PREF_CD_DAY_DATE, para_cd_day_date.getValue());
pref.putString(PREF_TAG_DAYS, para_tag_days.getValue());
pref.putString(PREF_SI_WEEK_1ST, strcmp(para_si_week_1st.getValue(), "1") == 0 ? "1" : "0");
pref.end();
Serial.println("Params saved.");
_idle_millis = millis(); // 刷新无操作时间点
ESP.restart();
}
void preSaveParamsCallback() {
}
// 双击打开配置页面
void buttonDoubleClick(void* oneButton) {
Serial.println("Button double click.");
if (wm.getConfigPortalActive()) {
ESP.restart();
return;
}
if (weather_status == 0) {
weather_stop();
}
// 设置配置页面
// 根据配置信息设置默认值
Preferences pref;
pref.begin(PREF_NAMESPACE);
String qHost = pref.getString(PREF_QWEATHER_HOST);
String qToken = pref.getString(PREF_QWEATHER_KEY);
String qType = pref.getString(PREF_QWEATHER_TYPE, "0");
String qLoc = pref.getString(PREF_QWEATHER_LOC);
String cddLabel = pref.getString(PREF_CD_DAY_LABLE);
String cddDate = pref.getString(PREF_CD_DAY_DATE);
String tagDays = pref.getString(PREF_TAG_DAYS);
String week1st = pref.getString(PREF_SI_WEEK_1ST, "0");
pref.end();
para_qweather_host.setValue(qHost.c_str(), 64);
para_qweather_key.setValue(qToken.c_str(), 32);
para_qweather_location.setValue(qLoc.c_str(), 64);
para_qweather_type.setValue(qType.c_str(), 1);
para_cd_day_label.setValue(cddLabel.c_str(), 16);
para_cd_day_date.setValue(cddDate.c_str(), 8);
para_tag_days.setValue(tagDays.c_str(), 30);
para_si_week_1st.setValue(week1st.c_str(), 1);
wm.setTitle("J-Calendar");
wm.addParameter(&para_si_week_1st);
wm.addParameter(&para_qweather_host);
wm.addParameter(&para_qweather_key);
wm.addParameter(&para_qweather_type);
wm.addParameter(&para_qweather_location);
wm.addParameter(&para_cd_day_label);
wm.addParameter(&para_cd_day_date);
wm.addParameter(&para_tag_days);
// std::vector<const char *> menu = {"wifi","wifinoscan","info","param","custom","close","sep","erase","update","restart","exit"};
std::vector<const char*> menu = { "wifi","param","update","sep","info","restart","exit" };
wm.setMenu(menu); // custom menu, pass vector
wm.setConfigPortalBlocking(false);
wm.setBreakAfterConfig(true);
wm.setPreSaveParamsCallback(preSaveParamsCallback);
wm.setSaveParamsCallback(saveParamsCallback);
wm.setSaveConnect(false); // 保存完wifi信息后是否自动连接设置为否以便于用户继续配置param。
wm.startConfigPortal("J-Calendar", "password");
led_config(); // LED 进入三快闪状态
// 控制配置超时180秒后休眠
_idle_millis = millis();
}
// 重置系统,并重启
void buttonLongPressStop(void* oneButton) {
Serial.println("Button long press.");
// 删除Preferencesnamespace下所有健值对。
Preferences pref;
pref.begin(PREF_NAMESPACE);
pref.clear();
pref.end();
ESP.restart();
}
#define uS_TO_S_FACTOR 1000000
#define TIMEOUT_TO_SLEEP 10 // seconds
time_t blankTime = 0;
void go_sleep() {
// 设置唤醒时间为下个偶数整点。
time_t now = time(NULL);
struct tm tmNow = { 0 };
// Serial.printf("Now: %ld -- %s\n", now, ctime(&now));
localtime_r(&now, &tmNow); // 时间戳转化为本地时间结构
uint64_t p;
// 根据配置情况来刷新如果未配置qweather信息则24小时刷新否则每2小时刷新
Preferences pref;
pref.begin(PREF_NAMESPACE);
String _qweather_key = pref.getString(PREF_QWEATHER_KEY, "");
pref.end();
if (_qweather_key.length() == 0 || weather_type() == 0) { // 没有配置天气或者使用按日天气,则第二天刷新。
Serial.println("Sleep to next day.");
now += 3600 * 24;
localtime_r(&now, &tmNow); // 将新时间转成tm
// Serial.printf("Set1: %ld -- %s\n", now, ctime(&now));
struct tm tmNew = { 0 };
tmNew.tm_year = tmNow.tm_year;
tmNew.tm_mon = tmNow.tm_mon; // 月份从0开始
tmNew.tm_mday = tmNow.tm_mday; // 日期
tmNew.tm_hour = 0; // 小时
tmNew.tm_min = 0; // 分钟
tmNew.tm_sec = 10; // 秒, 防止离线时出现时间误差所以延后10s
time_t set = mktime(&tmNew);
p = (uint64_t)(set - time(NULL));
Serial.printf("Sleep time: %ld seconds\n", p);
} else {
if (tmNow.tm_hour % 2 == 0) { // 将时间推后两个小时,偶整点刷新。
now += 7200;
} else {
now += 3600;
}
localtime_r(&now, &tmNow); // 将新时间转成tm
// Serial.printf("Set1: %ld -- %s\n", now, ctime(&now));
struct tm tmNew = { 0 };
tmNew.tm_year = tmNow.tm_year;
tmNew.tm_mon = tmNow.tm_mon; // 月份从0开始
tmNew.tm_mday = tmNow.tm_mday; // 日期
tmNew.tm_hour = tmNow.tm_hour; // 小时
tmNew.tm_min = 0; // 分钟
tmNew.tm_sec = 10; // 秒, 防止离线时出现时间误差所以延后10s
time_t set = mktime(&tmNew);
p = (uint64_t)(set - time(NULL));
Serial.printf("Sleep time: %ld seconds\n", p);
}
esp_sleep_enable_timer_wakeup(p * (uint64_t)uS_TO_S_FACTOR);
esp_sleep_enable_ext0_wakeup(PIN_BUTTON, 0);
// 省电考虑关闭RTC外设和存储器
// esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_OFF); // RTC IO, sensors and ULP, 注意由于需要按键唤醒所以不能关闭否则会导致RTC_IO唤醒失败
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF); //
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
// 省电考虑重置gpio平均每针脚能省8ua。
gpio_reset_pin(PIN_LED); // 减小deep-sleep电流
gpio_reset_pin(GPIO_NUM_5); // 减小deep-sleep电流
gpio_reset_pin(GPIO_NUM_17); // 减小deep-sleep电流
gpio_reset_pin(GPIO_NUM_16); // 减小deep-sleep电流
gpio_reset_pin(GPIO_NUM_4); // 减小deep-sleep电流
delay(10);
Serial.println("Deep sleep...");
Serial.flush();
esp_deep_sleep_start();
}

68
src/ota.cpp Normal file
View File

@@ -0,0 +1,68 @@
#include "ota.h"
#include <Arduino.h>
#include <HTTPUpdate.h>
String _ota_url = "http://bin.bemfa.com/b/=esp32bin.bin";
//当升级开始时,打印日志
void update_started() {
Serial.println("OTA: HTTP update process started");
}
//当升级结束时,打印日志
void update_finished() {
Serial.println("OTA: HTTP update process finished");
}
//当升级中,打印日志
void update_progress(int cur, int total) {
Serial.printf("OTA: HTTP update process at %d of %d bytes...\n", cur, total);
}
//当升级失败时,打印日志
void update_error(int err) {
Serial.printf("OTA: HTTP update fatal error code %d\n", err);
}
/**
* 固件升级
* 通过http请求获取远程固件实现升级
*/
void ota_update() {
Serial.println("start update");
if (!WiFi.isConnected()) {
WiFi.begin();
}
int i = 0;
while(WiFi.status() != WL_CONNECTED) {
if(i ++ > 5) {
break;
}
delay(1000);
}
if ((WiFi.status() == WL_CONNECTED)) {
Serial.println("start update");
WiFiClient UpdateClient;
httpUpdate.onStart(update_started);//当升级开始时
httpUpdate.onEnd(update_finished);//当升级结束时
httpUpdate.onProgress(update_progress);//当升级中
httpUpdate.onError(update_error);//当升级失败时
t_httpUpdate_return ret = httpUpdate.update(UpdateClient, _ota_url);
switch (ret) {
case HTTP_UPDATE_FAILED: //当升级失败
Serial.println("[update] Update failed.");
break;
case HTTP_UPDATE_NO_UPDATES: //当无升级
Serial.println("[update] No update.");
break;
case HTTP_UPDATE_OK: //当升级成功
Serial.println("[update] Update ok.");
break;
}
} else {
Serial.println("[update] Update failed due to no wifi.");
}
}

938
src/screen_ink.cpp Normal file
View File

@@ -0,0 +1,938 @@
#include "screen_ink.h"
#include <weather.h>
#include <API.hpp>
#include "holiday.h"
#include "nongli.h"
#include <_preference.h>
#include <U8g2_for_Adafruit_GFX.h>
#include <GxEPD2_3C.h>
#include "GxEPD2_display_selection_new_style.h"
#include "font.h"
#define ROTATION 0
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, GxEPD2_DRIVER_CLASS::HEIGHT> display(GxEPD2_DRIVER_CLASS(/*CS=D8*/ 5, /*DC=D3*/ 17, /*RST=D4*/ 16, /*BUSY=D2*/ 4));
U8G2_FOR_ADAFRUIT_GFX u8g2Fonts;
#define FONT_TEXT u8g2_font_wqy16_t_gb2312 // 224825bytes最大字库天气描述中“霾”只有此字库中有
#define FONT_SUB u8g2_font_wqy12_t_gb2312 // 次要字体u8g2最小字体
const String week_str[] = { "", "", "", "", "", "", "" };
// const String tg_str[] = { "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸" }; // 天干
// const String dz_str[] = { "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥" }; // 地支
// const String sx_str[] = { "鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪" }; // 生肖
const String nl10_str[] = { "", "", "廿", "" }; // 农历十位
const String nl_str[] = { "", "", "", "", "", "", "", "", "", "", "" }; // 农历个位
const String nl_mon_str[] = { "", "", "", "", "", "", "", "", "", "", "", "", "" }; // 农历首位
int _screen_status = -1;
int _calendar_status = -1;
String _cd_day_label;
String _cd_day_date;
String _tag_days_str;
int _week_1st;
int lunarDates[31];
int jqAccDate[24]; // 节气积累日
const int jrLength = 11;
const int jrDate[] = { 101, 214, 308, 312, 501, 504, 601, 701, 801, 910, 1001, 1224, 1225 };
const String jrText[] = { "元旦", "情人节", "妇女节", "植树节", "劳动节", "青年节", "儿童节", "建党节", "建军节", "教师节", "国庆节", "平安夜", "圣诞节" };
struct tm tmInfo = { 0 }; // 日历显示用的时间
struct
{
int16_t topX;
int16_t topY;
int16_t topW;
int16_t topH;
int16_t tX;
int16_t tY;
int16_t tW;
int16_t tH;
int16_t yearX;
int16_t yearY;
int16_t weekX;
int16_t weekY;
int16_t lunarYearX;
int16_t lunarYearY;
int16_t lunarDayX;
int16_t lunarDayY;
int16_t cdDayX;
int16_t cdDayY;
int16_t weatherX;
int16_t weatherY;
int16_t weatherW;
int16_t weatherH;
int16_t headerX;
int16_t headerY;
int16_t headerW;
int16_t headerH;
int16_t daysX;
int16_t daysY;
int16_t daysW;
int16_t daysH;
int16_t dayW;
int16_t dayH;
} static calLayout;
TaskHandle_t SCREEN_HANDLER;
void init_cal_layout_size() {
calLayout.topX = 0;
calLayout.topY = 0;
calLayout.topW = 180;
calLayout.topH = 60;
calLayout.yearX = 10;
calLayout.yearY = calLayout.topH - 28;
calLayout.weekX = 10;
calLayout.weekY = calLayout.topH - 5;
calLayout.lunarYearX = calLayout.topX + calLayout.topW;
calLayout.lunarYearY = calLayout.yearY / 2;
calLayout.lunarDayX = calLayout.topX + calLayout.topW;
calLayout.lunarDayY = calLayout.yearY;
calLayout.cdDayX = 0;
calLayout.cdDayY = calLayout.topH - 5;
calLayout.tX = calLayout.topX + calLayout.topW;
calLayout.tY = calLayout.topY;
calLayout.tW = 60;
calLayout.tH = calLayout.topH / 2;
calLayout.weatherX = 300;
calLayout.weatherY = calLayout.topY;
calLayout.weatherW = display.width() - calLayout.weatherX;
calLayout.weatherH = calLayout.topH;
calLayout.headerX = calLayout.topX;
calLayout.headerY = calLayout.topY + calLayout.topH;
calLayout.headerW = display.width();
calLayout.headerH = 20;
calLayout.daysX = calLayout.topX;
calLayout.daysY = calLayout.headerY + calLayout.headerH;
calLayout.daysW = calLayout.headerW;
calLayout.daysH = display.height() - calLayout.daysX;
calLayout.dayW = 56;
calLayout.dayH = 44;
}
void draw_cal_layout() {
uint16_t color;
u8g2Fonts.setFont(FONT_TEXT);
u8g2Fonts.setFontMode(1);
u8g2Fonts.setFontDirection(0);
u8g2Fonts.setForegroundColor(GxEPD_WHITE);
int16_t daysMagin = 4;
for (int i = 0; i < 7; i++) {
if((i + _week_1st) % 7 == 0 || (i + _week_1st) % 7 == 6) {
color = GxEPD_RED;
} else {
color = GxEPD_BLACK;
}
// header background
if(i == 0) {
display.fillRect(0, calLayout.headerY, (display.width() - 7 * calLayout.dayW)/2, calLayout.headerH, color);
} else if(i == 6) {
display.fillRect((display.width() + 7 * calLayout.dayW)/2, calLayout.headerY, (display.width() - 7 * calLayout.dayW)/2, calLayout.headerH, color);
}
display.fillRect((display.width() - 7 * calLayout.dayW)/2 + i * calLayout.dayW, calLayout.headerY, calLayout.dayW, calLayout.headerH, color);
// header text
u8g2Fonts.drawUTF8(calLayout.headerX + daysMagin + (calLayout.dayW - u8g2Fonts.getUTF8Width(week_str[i].c_str())) / 2 + i * calLayout.dayW, calLayout.headerY + calLayout.headerH - 3, week_str[(i + _week_1st) % 7].c_str());
}
}
uint16_t todayColor = GxEPD_BLACK;
String todayLunarYear;
String todayLunarDay;
// 更新年份
void draw_cal_year(bool partial) {
if (partial) {
display.setPartialWindow(calLayout.topX, calLayout.topY, calLayout.topW + calLayout.tW, calLayout.topH);
display.firstPage();
display.fillScreen(GxEPD_WHITE);
}
// 日期
u8g2Fonts.setFontMode(1);
u8g2Fonts.setFontDirection(0);
u8g2Fonts.setForegroundColor(todayColor);
u8g2Fonts.setBackgroundColor(GxEPD_WHITE);
u8g2Fonts.setCursor(calLayout.yearX, calLayout.yearY);
u8g2Fonts.setFont(u8g2_font_fub25_tn);
u8g2Fonts.print(String(tmInfo.tm_year + 1900).c_str());
u8g2Fonts.setFont(FONT_TEXT);
u8g2Fonts.setForegroundColor(GxEPD_BLACK);
u8g2Fonts.print("");
u8g2Fonts.setFont(u8g2_font_fub25_tn);
u8g2Fonts.setForegroundColor(todayColor);
u8g2Fonts.print(String(tmInfo.tm_mon + 1).c_str());
u8g2Fonts.setFont(FONT_TEXT);
u8g2Fonts.setForegroundColor(GxEPD_BLACK);
u8g2Fonts.print("");
u8g2Fonts.setFont(u8g2_font_fub25_tn);
u8g2Fonts.setForegroundColor(todayColor);
u8g2Fonts.printf(String(tmInfo.tm_mday).c_str());
u8g2Fonts.setFont(FONT_TEXT);
u8g2Fonts.setForegroundColor(GxEPD_BLACK);
u8g2Fonts.print("");
calLayout.lunarYearX = u8g2Fonts.getCursorX() + 15;
calLayout.lunarDayX = u8g2Fonts.getCursorX() + 15;
// 星期几
u8g2Fonts.setFont(FONT_TEXT);
u8g2Fonts.setFontMode(1);
u8g2Fonts.setFontDirection(0);
u8g2Fonts.setForegroundColor(todayColor);
u8g2Fonts.setCursor(calLayout.weekX, calLayout.weekY);
u8g2Fonts.print("星期" + week_str[tmInfo.tm_wday]);
calLayout.cdDayX = u8g2Fonts.getCursorX(); // update cd day X;
// 今日农历年份e.g. 乙巳年 蛇
// 如果农历月份小于公历月份,那么说明是上一年
u8g2Fonts.setCursor(calLayout.lunarYearX, calLayout.lunarYearY);
u8g2Fonts.setFont(FONT_SUB);
u8g2Fonts.setFontMode(1);
u8g2Fonts.setFontDirection(0);
u8g2Fonts.setForegroundColor(GxEPD_BLACK);
u8g2Fonts.print(todayLunarYear);
// 今日农历日期
u8g2Fonts.setCursor(calLayout.lunarDayX, calLayout.lunarDayY);
u8g2Fonts.setFont(FONT_SUB);
u8g2Fonts.setForegroundColor(GxEPD_BLACK);
u8g2Fonts.print(todayLunarDay);
// 特殊日期
// draw_special_day();
if (partial) {
display.nextPage();
}
}
void draw_cal_days(bool partial) {
if (partial) {
display.setPartialWindow(calLayout.daysX, calLayout.daysY, calLayout.daysW, calLayout.daysH);
display.firstPage();
display.fillScreen(GxEPD_WHITE);
}
u8g2Fonts.setFontMode(1);
u8g2Fonts.setFontDirection(0);
u8g2Fonts.setForegroundColor(GxEPD_BLACK);
size_t totalDays = 30; // 小月
int monthNum = tmInfo.tm_mon + 1;
if (monthNum == 1 || monthNum == 3 || monthNum == 5 || monthNum == 7 || monthNum == 8 || monthNum == 10 || monthNum == 12) { // 大月
totalDays = 31;
}
if (monthNum == 2) {
if ((tmInfo.tm_year + 1900) == 0 && (tmInfo.tm_year + 1900) % 100 != 0) {
totalDays = 29; // 闰二月
} else {
totalDays = 28; // 二月
}
}
// 计算本月第一天星期几
int wday1 = (36 - tmInfo.tm_mday + tmInfo.tm_wday) % 7;
// 计算本月第一天是全年的第几天0365
int yday1 = tmInfo.tm_yday - tmInfo.tm_mday + 1;
// 确认哪些日期需要打tag
char tags[31] = { 0 };
int indexBegin = 0;
while (_tag_days_str.length() >= (indexBegin + 9)) {
String y = _tag_days_str.substring(indexBegin, indexBegin + 4);
String m = _tag_days_str.substring(indexBegin + 4, indexBegin + 6);
String d = _tag_days_str.substring(indexBegin + 6, indexBegin + 8);
char t = _tag_days_str.charAt(indexBegin + 8);
if ((y.equals(String(tmInfo.tm_year + 1900)) || y.equals("0000")) && (m.equals(String(tmInfo.tm_mon + 1)) || m.equals("00"))) {
tags[d.toInt()] = t;
}
// Serial.printf("Format: %s, %s, %s, %c\n", y.c_str(), m.c_str(), d.c_str(), t);
indexBegin = indexBegin + 9;
while (indexBegin < _tag_days_str.length() && (_tag_days_str.charAt(indexBegin) < '0' || _tag_days_str.charAt(indexBegin) > '9')) { // 搜索字符串直到下个字符是0-9之间的
indexBegin++;
}
}
Holiday _holiday;
Preferences pref;
pref.begin(PREF_NAMESPACE);
size_t holiday_size = pref.getBytesLength(PREF_HOLIDAY);
if (holiday_size > 0) {
pref.getBytes(PREF_HOLIDAY, &_holiday, holiday_size);
}
pref.end();
if(_holiday.year != tmInfo.tm_year + 1900 || _holiday.month != tmInfo.tm_mon + 1) {
_holiday = {};
}
int jqIndex = 0;
int jrIndex = 0;
int shiftDay = (wday1 - _week_1st) >= 0 ? 0 : 7;
for (size_t iDay = 0; iDay < totalDays; iDay++) {
uint8_t num = wday1 + iDay - _week_1st + shiftDay; // 根据每周首日星期做偏移
uint8_t column = num % 7; //(0~6)
uint8_t row = num / 7; //(0~4)
if (row == 5) row = 0;
int16_t x = calLayout.daysX + 4 + column * 56;
int16_t y = calLayout.daysY + row * 44;
// 周六、日,字体红色
uint16_t color;
if ((wday1 + iDay) % 7 == 0 || (wday1 + iDay) % 7 == 6) {
color = GxEPD_RED;
} else {
color = GxEPD_BLACK;
}
if (tmInfo.tm_year + 1900 == _holiday.year && tmInfo.tm_mon + 1 == _holiday.month) {
uint8_t holidayIndex = 0;
for (; holidayIndex < _holiday.length; holidayIndex++) {
if (abs(_holiday.holidays[holidayIndex]) == (iDay + 1)) {
// 显示公休、调班logo和颜色
u8g2Fonts.setFont(u8g2_font_open_iconic_all_1x_t);
if (_holiday.holidays[holidayIndex] > 0) { // 公休
color = GxEPD_RED;
u8g2Fonts.setForegroundColor(color);
u8g2Fonts.setBackgroundColor(GxEPD_WHITE);
u8g2Fonts.drawUTF8(x + 44, y + 11, "\u006c");
} else if (_holiday.holidays[holidayIndex] < 0) { // 调班
color = GxEPD_BLACK;
u8g2Fonts.setForegroundColor(color);
u8g2Fonts.setBackgroundColor(GxEPD_WHITE);
u8g2Fonts.drawUTF8(x + 44, y + 11, "\u0064");
}
break;
}
}
}
u8g2Fonts.setForegroundColor(color); // 设置整体颜色
u8g2Fonts.setBackgroundColor(GxEPD_WHITE);
// 画日历日期数字
u8g2Fonts.setFont(u8g2_font_fub17_tn); // u8g2_font_fub17_tnu8g2_font_logisoso18_tn
int16_t numX = x + (56 - u8g2Fonts.getUTF8Width(String(iDay + 1).c_str())) / 2;
int16_t numY = y + 22;
u8g2Fonts.drawUTF8(numX, numY, String(iDay + 1).c_str()); // 画日历日期
// 画节气&节日&农历
String lunarStr = "";
int lunarDate = lunarDates[iDay];
int isLeapMon = lunarDate < 0 ? 1 : 0; // 闰月
lunarDate = abs(lunarDate);
int lunarMon = lunarDate / 100;
int lunarDay = lunarDate % 100;
bool isJq = false; // 是否节气
int accDays0 = tmInfo.tm_yday + 1 - tmInfo.tm_mday; // 本月0日的积累日tm_yday 从0开始tm_mday从1开始, i从0开始
for (; jqIndex < 24; jqIndex++) {
if (accDays0 + iDay + 1 < jqAccDate[jqIndex]) {
break;
}
if (accDays0 + iDay + 1 == jqAccDate[jqIndex]) {
lunarStr = String(nl_jq_text[jqIndex]);
isJq = true;
break;
}
}
bool isJr = false; // 是否节日
for (; jrIndex < jrLength; jrIndex++) {
if (tmInfo.tm_mon * 100 + iDay + 1 < jrDate[jrIndex]) {
break;
}
if (tmInfo.tm_mon * 100 + iDay + 1 < jrDate[jrIndex]) {
lunarStr = jrText[jrIndex];
isJr == true;
break;
}
}
if (!isJq && !isJr) { // 农历
if (lunarDay == 1) {
// 初一,显示月份
lunarStr = (isLeapMon == 0 ? "" : "") + nl_mon_str[lunarMon] + "";
} else {
if (lunarDay == 10) {
lunarStr = "初十";
} else if (lunarDay == 20) {
lunarStr = "二十";
} else if (lunarDay == 30) {
lunarStr = "三十";
} else {
// 其他日期
lunarStr = nl10_str[lunarDay / 10] + nl_str[lunarDay % 10];
}
}
if (lunarMon == 1 && lunarDay == 1) {
lunarStr = "春节";
} else if (lunarMon == 1 && lunarDay == 15) {
lunarStr = "元宵节";
} else if (lunarMon == 5 && lunarDay == 5) {
lunarStr = "端午节";
} else if (lunarMon == 7 && lunarDay == 7) {
lunarStr = "七夕节";
} else if (lunarMon == 8 && lunarDay == 15) {
lunarStr = "中秋节";
} else if (lunarMon == 9 && lunarDay == 9) {
lunarStr = "重阳节";
}
}
// 画节气/节日/农历文字
u8g2Fonts.setFont(FONT_TEXT);
u8g2Fonts.drawUTF8(x + (56 - u8g2Fonts.getUTF8Width(lunarStr.c_str())) / 2, y + 44 - 4, lunarStr.c_str());
// 今日日期
if ((iDay + 1) == tmInfo.tm_mday) { // 双线加粗
todayColor = color;
// 加框线
display.drawRoundRect(x, y + 1, 56, 44, 4, GxEPD_RED);
display.drawRoundRect(x + 1, y + 2, 54, 42, 3, GxEPD_RED);
// 今日农历年份e.g. 乙巳年 蛇
// 如果农历月份小于公历月份,那么说明是上一年
int tg = nl_tg(tmInfo.tm_year + 1900 - (lunarMon > (tmInfo.tm_mon + 1)? 1:0));
int dz = nl_dz(tmInfo.tm_year + 1900 - (lunarMon > (tmInfo.tm_mon + 1)? 1:0));;
todayLunarYear = String(nl_tg_text[tg]) + String(nl_dz_text[dz]) + "" + String(nl_sx_text[dz]);
// 今日农历日期
if (lunarDay == 10) {
lunarStr = "初十";
} else if (lunarDay == 20) {
lunarStr = "二十";
} else if (lunarDay == 30) {
lunarStr = "三十";
} else {
// 其他日期
lunarStr = nl10_str[lunarDay / 10] + nl_str[lunarDay % 10];
}
todayLunarDay = (isLeapMon == 0 ? "" : "") + nl_mon_str[lunarMon] + "" + lunarStr;
}
// 画日期Tag
const char* tagChar = NULL;
if (tags[iDay + 1] == 'a') { //tag
tagChar = "\u0042";
} else if (tags[iDay + 1] == 'b') { // dollar
tagChar = "\u0024";
} else if (tags[iDay + 1] == 'c') { // smile
tagChar = "\u0053";
} else if (tags[iDay + 1] == 'd') { // warning
tagChar = "\u0021";
}
if (tagChar != NULL) {
u8g2Fonts.setBackgroundColor(GxEPD_WHITE);
u8g2Fonts.setForegroundColor(GxEPD_RED);
u8g2Fonts.setFont(u8g2_font_twelvedings_t_all);
int iconX = numX - u8g2Fonts.getUTF8Width(tagChar) - 1; // 数字与tag间间隔1像素
iconX = iconX <= (x + 3) ? (iconX + 1) : iconX; // 防止icon与今日框线产生干涉。
int iconY = y + 15;
u8g2Fonts.drawUTF8(iconX, iconY, tagChar);
}
// 画Calendar提示点
/*
Calendar* cal = weather_cal();
for(int calIndex = 0; calIndex < cal->length; calIndex ++) {
CalEvent event = cal->events[calIndex];
if(event.dt_begin.substring(0, 4).toInt() == (tmInfo.tm_year + 1900)
&& event.dt_begin.substring(4, 6).toInt() == (tmInfo.tm_mon + 1)
&& event.dt_begin.substring(6, 8).toInt() == (iDay + 1)) {
u8g2Fonts.setBackgroundColor(GxEPD_WHITE);
u8g2Fonts.setForegroundColor(GxEPD_RED);
u8g2Fonts.setFont(u8g2_font_siji_t_6x10);
u8g2Fonts.drawUTF8(x + 42, y + 12, "\ue015");
}
}
*/
}
}
// draw countdown-day info
void draw_cd_day(String label, String date) {
if (label == NULL || date == NULL || label.length() == 0 || date.length() != 8) {
Serial.print("Invalid countdown-day parameters.\n");
return;
}
long d = atol(date.c_str());
struct tm today = { 0 }; // 今日0秒
today.tm_year = tmInfo.tm_year;
today.tm_mon = tmInfo.tm_mon;
today.tm_mday = tmInfo.tm_mday;
today.tm_hour = 0;
today.tm_min = 0;
today.tm_sec = 0;
time_t todayT = mktime(&today);
struct tm someday = { 0 }; // 倒计日0秒
someday.tm_year = d / 10000 - 1900;
someday.tm_mon = d % 10000 / 100 - 1;
someday.tm_mday = d % 100;
someday.tm_hour = 0;
someday.tm_min = 0;
someday.tm_sec = 0;
time_t somedayT = mktime(&someday);
/*
char buffer[80];
strftime(buffer, 80, "%Y-%m-%d %H:%M:%S", &someday);
Serial.printf("CD day: %s\n", buffer);
*/
long diff = somedayT - todayT;
if (diff < 0) return; // 倒计日已过
int16_t beginX = calLayout.cdDayX;
int16_t endX = calLayout.weatherX;
int16_t y = calLayout.cdDayY;
if (diff == 0) {
String prefix = "今日 ";
String suffix = " ";
u8g2Fonts.setFont(FONT_SUB);
int16_t preWidth = u8g2Fonts.getUTF8Width(prefix.c_str());
int16_t suffixWidth = u8g2Fonts.getUTF8Width(suffix.c_str());
u8g2Fonts.setFont(FONT_SUB);
int16_t labelWidth = u8g2Fonts.getUTF8Width(label.c_str());
int16_t margin = (endX - beginX - preWidth - labelWidth - suffixWidth) / 2;
u8g2Fonts.setCursor((margin > 0 ? margin : 0) + beginX, y); // 居中显示
u8g2Fonts.setForegroundColor(GxEPD_BLACK);
u8g2Fonts.setFont(FONT_SUB);
u8g2Fonts.print(prefix.c_str()); // 今天
u8g2Fonts.setForegroundColor(GxEPD_RED);
u8g2Fonts.setFont(FONT_TEXT);
u8g2Fonts.print(label.c_str()); // ****
u8g2Fonts.setForegroundColor(GxEPD_BLACK);
u8g2Fonts.setFont(FONT_SUB);
u8g2Fonts.print(suffix.c_str()); //
} else if (diff > 0) {
String prefix = "";
String middle = " 还有 ";
int iDiff = diff / (60 * 60 * 24);
char days[11] = "";
itoa(iDiff, days, 10);
String suffix = "";
u8g2Fonts.setFont(FONT_SUB);
int16_t preWidth = u8g2Fonts.getUTF8Width(prefix.c_str());
int16_t midWidth = u8g2Fonts.getUTF8Width(middle.c_str());
int16_t suffixWidth = u8g2Fonts.getUTF8Width(suffix.c_str());
u8g2Fonts.setFont(FONT_SUB);
int16_t labelWidth = u8g2Fonts.getUTF8Width(label.c_str());
u8g2Fonts.setFont(u8g2_font_fub14_tn);
int16_t daysWidth = u8g2Fonts.getUTF8Width(days);
int16_t margin = (endX - beginX - preWidth - labelWidth - midWidth - daysWidth - suffixWidth) / 2;
u8g2Fonts.setCursor((margin > 0 ? margin : 0) + beginX, y); // 居中显示
u8g2Fonts.setForegroundColor(GxEPD_BLACK);
u8g2Fonts.setFont(FONT_SUB);
u8g2Fonts.print(prefix.c_str()); // 距
u8g2Fonts.setFont(FONT_TEXT);
u8g2Fonts.print(label.c_str()); // ****
u8g2Fonts.setFont(FONT_SUB);
u8g2Fonts.print(middle.c_str()); // 还有
u8g2Fonts.setForegroundColor(GxEPD_RED);
u8g2Fonts.setFont(u8g2_font_fub14_tn);
u8g2Fonts.print(days); // 0000
u8g2Fonts.setForegroundColor(GxEPD_BLACK);
u8g2Fonts.setFont(FONT_SUB);
u8g2Fonts.print(suffix.c_str()); // 天
}
}
void draw_special_day() {
String str = "Special Days!!!";
u8g2Fonts.setCursor(u8g2Fonts.getCursorX() + 12, u8g2Fonts.getCursorY());
u8g2Fonts.setForegroundColor(GxEPD_RED);
u8g2Fonts.setFont(u8g2_font_open_iconic_all_2x_t);
u8g2Fonts.print("\u00b7"); // 爱心
u8g2Fonts.setFont(FONT_TEXT);
u8g2Fonts.setForegroundColor(GxEPD_BLACK);
u8g2Fonts.print(str.c_str());
u8g2Fonts.setForegroundColor(GxEPD_RED);
u8g2Fonts.setFont(u8g2_font_open_iconic_all_2x_t);
u8g2Fonts.print("\u00b7"); // 爱心
}
bool isNight(String time) {
uint8_t hour = time.substring(11, 13).toInt();
return hour < 6 || hour >= 18;
}
const char* getWeatherIcon(uint16_t id, bool fill) {
switch (id) {
case 100: return !fill ? "\uf101" : "\uf1ac";
case 101: return !fill ? "\uf102" : "\uf1ad";
case 102: return !fill ? "\uf103" : "\uf1ae";
case 103: return !fill ? "\uf104" : "\uf1af";
case 104: return !fill ? "\uf105" : "\uf1b0";
case 150: return !fill ? "\uf106" : "\uf1b1";
case 151: return !fill ? "\uf107" : "\uf1b2";
case 152: return !fill ? "\uf108" : "\uf1b3";
case 153: return !fill ? "\uf109" : "\uf1b4";
case 300: return !fill ? "\uf10a" : "\uf1b5";
case 301: return !fill ? "\uf10b" : "\uf1b6";
case 302: return !fill ? "\uf10c" : "\uf1b7";
case 303: return !fill ? "\uf10d" : "\uf1b8";
case 304: return !fill ? "\uf10e" : "\uf1b9";
case 305: return !fill ? "\uf10f" : "\uf1ba";
case 306: return !fill ? "\uf110" : "\uf1bb";
case 307: return !fill ? "\uf111" : "\uf1bc";
case 308: return !fill ? "\uf112" : "\uf1bd";
case 309: return !fill ? "\uf113" : "\uf1be";
case 310: return !fill ? "\uf114" : "\uf1bf";
case 311: return !fill ? "\uf115" : "\uf1c0";
case 312: return !fill ? "\uf116" : "\uf1c1";
case 313: return !fill ? "\uf117" : "\uf1c2";
case 314: return !fill ? "\uf118" : "\uf1c3";
case 315: return !fill ? "\uf119" : "\uf1c4";
case 316: return !fill ? "\uf11a" : "\uf1c5";
case 317: return !fill ? "\uf11b" : "\uf1c6";
case 318: return !fill ? "\uf11c" : "\uf1c7";
case 350: return !fill ? "\uf11d" : "\uf1c8";
case 351: return !fill ? "\uf11e" : "\uf1c9";
case 399: return !fill ? "\uf11f" : "\uf1ca";
case 400: return !fill ? "\uf120" : "\uf1cb";
case 401: return !fill ? "\uf121" : "\uf1cc";
case 402: return !fill ? "\uf122" : "\uf1cd";
case 403: return !fill ? "\uf123" : "\uf1ce";
case 404: return !fill ? "\uf124" : "\uf1cf";
case 405: return !fill ? "\uf125" : "\uf1d0";
case 406: return !fill ? "\uf126" : "\uf1d1";
case 407: return !fill ? "\uf127" : "\uf1d2";
case 408: return !fill ? "\uf128" : "\uf1d3";
case 409: return !fill ? "\uf129" : "\uf1d4";
case 410: return !fill ? "\uf12a" : "\uf1d5";
case 456: return !fill ? "\uf12b" : "\uf1d6";
case 457: return !fill ? "\uf12c" : "\uf1d7";
case 499: return !fill ? "\uf12d" : "\uf1d8";
case 500: return !fill ? "\uf12e" : "\uf1d9";
case 501: return !fill ? "\uf12f" : "\uf1da";
case 502: return !fill ? "\uf130" : "\uf1db";
case 503: return !fill ? "\uf131" : "\uf1dc";
case 504: return !fill ? "\uf132" : "\uf1dd";
case 507: return !fill ? "\uf133" : "\uf1de";
case 508: return !fill ? "\uf134" : "\uf1df";
case 509: return !fill ? "\uf135" : "\uf1e0";
case 510: return !fill ? "\uf136" : "\uf1e1";
case 511: return !fill ? "\uf137" : "\uf1e2";
case 512: return !fill ? "\uf138" : "\uf1e3";
case 513: return !fill ? "\uf139" : "\uf1e4";
case 514: return !fill ? "\uf13a" : "\uf1e5";
case 515: return !fill ? "\uf13b" : "\uf1e6";
case 800: return "\uf13c";
case 801: return "\uf13d";
case 802: return "\uf13e";
case 803: return "\uf13f";
case 804: return "\uf140";
case 805: return "\uf141";
case 806: return "\uf142";
case 807: return "\uf143";
case 900: return !fill ? "\uf144" : "\uf1e7";
case 901: return !fill ? "\uf145" : "\uf1e8";
case 999:
default: return !fill ? "\uf146" : "\uf1e9";
}
}
// 画天气信息
#include "API.hpp"
void draw_weather(bool partial) {
if (partial) {
display.setPartialWindow(calLayout.weatherX, calLayout.weatherY, calLayout.weatherW, calLayout.weatherH - 1); // 高度减1防止干扰到其他区域颜色
display.firstPage();
display.fillScreen(GxEPD_WHITE);
}
u8g2Fonts.setFontMode(1);
u8g2Fonts.setFontDirection(0);
u8g2Fonts.setForegroundColor(GxEPD_BLACK);
u8g2Fonts.setBackgroundColor(GxEPD_WHITE);
if (weather_type() == 1) {
// 实时天气
Weather* wNow = weather_data_now();
/* 更新排版,将字体缩小,与每日天气风格一致
// 天气图标
u8g2Fonts.setFont(u8g2_font_qweather_icon_16);
u8g2Fonts.setCursor(calLayout.weatherX + 2, calLayout.weatherY + 30);
u8g2Fonts.print(getWeatherIcon(wNow->icon, isNight(wNow->time)));
// 天气文字
u8g2Fonts.setFont(FONT_TEXT);
uint16_t w1 = u8g2Fonts.getUTF8Width(wNow->text.c_str());
u8g2Fonts.setCursor(calLayout.weatherX + 30, calLayout.weatherY + 18);
u8g2Fonts.print((wNow->text).c_str());
// 温湿度
u8g2Fonts.setFont(u8g2_font_tenthinnerguys_tf);
u8g2Fonts.setCursor(calLayout.weatherX + 30, calLayout.weatherY + 35);
u8g2Fonts.printf("%d°C | %d%%", wNow->temp, wNow->humidity);
// 风向级别
u8g2Fonts.setCursor(calLayout.weatherX, calLayout.weatherY + calLayout.weatherH - 6);
u8g2Fonts.setFont(FONT_TEXT);
u8g2Fonts.printf("%s", wNow->windDir.c_str());
u8g2Fonts.setFont(u8g2_font_fub14_tn);
u8g2Fonts.printf(" %d ", wNow->windScale);
u8g2Fonts.setFont(FONT_TEXT);
u8g2Fonts.printf("级");
*/
// 天气图标
u8g2Fonts.setFont(u8g2_font_qweather_icon_16);
u8g2Fonts.setCursor(calLayout.weatherX, calLayout.weatherY + 44);
u8g2Fonts.print(getWeatherIcon(wNow->icon, isNight(wNow->time)));
// 天气文字
u8g2Fonts.setFont(FONT_SUB);
uint16_t w1 = u8g2Fonts.getUTF8Width(wNow->text.c_str());
u8g2Fonts.setCursor(calLayout.weatherX + 28, calLayout.weatherY + 22);
u8g2Fonts.print(wNow->text.c_str());
u8g2Fonts.setFont(u8g2_font_tenthinnerguys_tf);
u8g2Fonts.setCursor(calLayout.weatherX + 28, calLayout.weatherY + 37);
u8g2Fonts.printf("%d°C | %d%%", wNow->temp, wNow->humidity);
// 风向级别
u8g2Fonts.setCursor(calLayout.weatherX + 28, calLayout.weatherY + calLayout.weatherH - 6);
u8g2Fonts.setFont(FONT_SUB);
u8g2Fonts.printf("%s", wNow->windDir.c_str());
u8g2Fonts.setCursor(u8g2Fonts.getCursorX() + 5, u8g2Fonts.getCursorY());
u8g2Fonts.setFont(u8g2_font_tenthinnerguys_tf);
u8g2Fonts.printf("%d", wNow->windScale);
u8g2Fonts.setCursor(u8g2Fonts.getCursorX() + 5, u8g2Fonts.getCursorY());
u8g2Fonts.setFont(FONT_SUB);
u8g2Fonts.printf("");
} else {
// 每日天气
DailyForecast* wFc = weather_data_daily();
DailyWeather* dw = wFc->weather;
if (wFc->length == 0) {
return;
}
DailyWeather wToday = dw[0];
// 天气图标
u8g2Fonts.setFont(u8g2_font_qweather_icon_16);
u8g2Fonts.setCursor(calLayout.weatherX, calLayout.weatherY + 44);
u8g2Fonts.print(getWeatherIcon(wToday.iconDay, false));
// 天气文字
u8g2Fonts.setFont(FONT_SUB);
uint16_t w1 = u8g2Fonts.getUTF8Width(wToday.textDay.c_str());
u8g2Fonts.setCursor(calLayout.weatherX + 28, calLayout.weatherY + 22);
u8g2Fonts.print((wToday.textDay).c_str());
u8g2Fonts.setFont(u8g2_font_tenthinnerguys_tf);
u8g2Fonts.setCursor(u8g2Fonts.getCursorX() + 5, calLayout.weatherY + 22);
u8g2Fonts.printf("%d%%", wToday.humidity);
// 温度
u8g2Fonts.setFont(u8g2_font_tenthinnerguys_tf);
u8g2Fonts.setCursor(calLayout.weatherX + 28, calLayout.weatherY + 37);
u8g2Fonts.printf("%d - %d°C", wToday.tempMin, wToday.tempMax);
// 风向级别
u8g2Fonts.setCursor(calLayout.weatherX + 28, calLayout.weatherY + calLayout.weatherH - 6);
u8g2Fonts.setFont(FONT_SUB);
u8g2Fonts.printf("%s", wToday.windDirDay.c_str());
u8g2Fonts.setFont(u8g2_font_tenthinnerguys_tf);
u8g2Fonts.printf(" %d ", wToday.windScaleDay);
u8g2Fonts.setFont(FONT_SUB);
u8g2Fonts.printf("");
}
if (partial) {
display.nextPage();
}
}
// Draw err
void draw_err(bool partial) {
if (partial) {
display.setPartialWindow(380, 0, 20, 20);
display.firstPage();
display.fillScreen(GxEPD_WHITE);
}
u8g2Fonts.setFontMode(1);
u8g2Fonts.setFontDirection(0);
u8g2Fonts.setBackgroundColor(GxEPD_WHITE);
u8g2Fonts.setForegroundColor(GxEPD_RED);
u8g2Fonts.setFont(u8g2_font_open_iconic_all_2x_t);
u8g2Fonts.setCursor(382, 18);
u8g2Fonts.print("\u0118");
if (partial) {
display.nextPage();
}
}
///////////// Calendar //////////////
/**
* 处理日历信息
*/
void si_calendar() {
_calendar_status = 0;
Preferences pref;
pref.begin(PREF_NAMESPACE);
int32_t _calendar_date = pref.getInt(PREF_SI_CAL_DATE);
_cd_day_label = pref.getString(PREF_CD_DAY_LABLE);
_cd_day_date = pref.getString(PREF_CD_DAY_DATE);
_tag_days_str = pref.getString(PREF_TAG_DAYS);
_week_1st = pref.getString(PREF_SI_WEEK_1ST, "0").toInt();
pref.end();
time_t now = 0;
time(&now);
localtime_r(&now, &tmInfo); // 时间戳转化为本地时间结构
Serial.printf("System Time: %d-%02d-%02d %02d:%02d:%02d\n", (tmInfo.tm_year + 1900), tmInfo.tm_mon + 1, tmInfo.tm_mday, tmInfo.tm_hour, tmInfo.tm_min, tmInfo.tm_sec);
// 如果当前时间无效
if (tmInfo.tm_year + 1900 < 2025) {
bool isSetOK = false;
if (weather_status() == 1) {
// 尝试使用api获取的时间
String apiTime;
Weather* weatherNow = weather_data_now();
if (weatherNow->updateTime == NULL) {
// TODO 处理每日天气
DailyForecast* wFc = weather_data_daily();
apiTime = wFc->updateTime;
} else {
apiTime = weatherNow->updateTime;
}
Serial.printf("API Time: %s\n", apiTime.c_str());
tmInfo = { 0 }; // 重置为0
if (strptime(apiTime.c_str(), "%Y-%m-%dT%H:%M", &tmInfo) != NULL) { // 将时间字符串转成tm时间 e.g. 2024-11-14T17:36+08:00
time_t set = mktime(&tmInfo);
timeval tv;
tv.tv_sec = set;
settimeofday(&tv, nullptr);
isSetOK = true;
Serial.println("WARN: Set system time by api time.");
} else {
Serial.println("ERR: Fail to format api time.");
}
} else {
// 如果天气也未获取成功,那么返回
Serial.println("ERR: invalid time & not weather info got.");
}
if (!isSetOK) {
_calendar_status = 2;
return;
}
}
char buffer[64];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tmInfo);
Serial.printf("Calendar Show Time: %s\n", buffer);
nl_month_days(tmInfo.tm_year + 1900, tmInfo.tm_mon + 1, lunarDates);
nl_year_jq(tmInfo.tm_year + 1900, jqAccDate);
_calendar_status = 1;
return;
}
int si_calendar_status() {
return _calendar_status;
}
///////////// Screen //////////////
/**
* 屏幕刷新
*/
void task_screen(void* param) {
Serial.println("[Task] screen update begin...");
display.init(115200); // 串口使能 初始化完全刷新使能 复位时间 ret上拉使能
display.setRotation(ROTATION); // 设置屏幕旋转1和3是横向 0和2是纵向
u8g2Fonts.begin(display);
init_cal_layout_size();
display.setFullWindow();
display.fillScreen(GxEPD_WHITE);
draw_cal_layout();
draw_cal_days(false);
draw_cal_year(false);
// 倒计日
draw_cd_day(_cd_day_label, _cd_day_date);
if (weather_status() == 1) {
draw_weather(false);
} else if (weather_status() == 2) {
draw_err(false);
}
display.display();
int32_t _calendar_date = (tmInfo.tm_year + 1900) * 10000 + (tmInfo.tm_mon + 1) * 100 + tmInfo.tm_mday;
Preferences pref;
pref.begin(PREF_NAMESPACE);
pref.putInt(PREF_SI_CAL_DATE, _calendar_date);
pref.end();
display.powerOff(); // !!!important!!!, 关闭屏幕否则会多0.5ma的空载电流(全屏刷新的话会自动关闭,局部刷新必须手动关闭)
Serial.println("[Task] screen update end...");
_screen_status = 1;
SCREEN_HANDLER = NULL;
vTaskDelete(NULL);
}
void si_screen() {
_screen_status = 0;
si_calendar(); // 准备日历数据
if (si_calendar_status() == 2) {
Serial.println("ERR: System time prepare failed.");
_screen_status = 2;
return;
}
if (SCREEN_HANDLER != NULL) {
vTaskDelete(SCREEN_HANDLER);
}
xTaskCreate(task_screen, "Screen", 4096, NULL, 2, &SCREEN_HANDLER);
}
int si_screen_status() {
return _screen_status;
}
void print_status() {
Serial.printf("Weather: %d\n", weather_status());
Serial.printf("Calendar: %d\n", si_calendar_status());
Serial.printf("Screen: %d\n", si_screen_status());
}

227
src/u8g2_font_qweather_icon_16.c Executable file
View File

@@ -0,0 +1,227 @@
/*
Fontname: -FreeType-qweather icons-Medium-R-Normal--22-160-100-100-P-208-ISO10646-1
Copyright: QWeather
Glyphs: 132/233
BBX Build Mode: 0
*/
#include "font.h"
#ifdef U8G2_USE_LARGE_FONTS
const uint8_t u8g2_font_qweather_icon_16[6862] U8G2_FONT_SECTION("u8g2_font_qweather_icon_16") =
"\204\0\5\4\5\5\4\4\6\27\30\0\377\0\0\0\0\0\0\0\0\0\2\0\0\0\4\377\377\361\1\67"
"\326\42\332+\324\250\5\27\216X\342\10a\204X\202\210&NX\2\11\25\225!\202\12b\224\240bJ"
" \261\204\21M\20\261\204\60B \261\302\21-\270P#\5\0\361\2@\366\342\331'\324P\302\13D"
"L\241\4!M\210\343\4\21M\30\21\6\22\62\4\61\12\32A\204\61\6\23G \222\10\21O\10\21"
"E\10\63\204\60C\10\63\4\21\205\60\307\20S\214:q$\0\361\3\77\326\342\331+\324\250\211\26\216"
"X\2\5a\204Xb\214&\214X\2\11\25\222PQ\31\42\250 F\11*(QE!L\210!D"
"\12\203\224Q\304\20I\14A\302\12I\250\240\6!\216\24\0\361\4\77\326\342\331+\324\250\5\27\214`"
"\342\210A\206X\250\215\42\230\70a\11$\224H\241\14!R\10\244\210\24VHa\211a\32!\245\20"
"&\6qa\204\30F\20\241\211Q\304\20\6\221@\14\0\361\5\60V\242\332\252@\61\4\33g \261"
"\204\21\302\24Q\202\20d\220p\304\11$\214\202\312\30n\4!\7\225\350\230!\4\31\206\61&!H"
"\22\0\361\6,\61\256\336E\270\301H\23!\60!\304\12C(A\202\22D$QB\22E q\204"
"\21\350\4\301D\20K\14\221D\231\320)\0\361\7\66\266\42\332-P\61\207\24!\310 D\14B\304"
"\60D\25\255\220b\306\30E\30i\10B\20!\303\211\21\242\10B\206 d\20!\12!` \306\24"
"u\342P\0\361\10;\264\346\331)\304\1E\10O\210\360\302\10N\214\340\2\21-\224\320B\21,\30"
"\261\302\31$JF\12R\220\30D\10#\4)C\214!\222\10\202\204\25\216P\1\21\62XY\0\361"
"\11\77\267\42\336JL\62E\10R\210 \303\20P\14\1\5\21O\20\361D\31M\30B\302\11\350\34"
"\301\202\62E\244!\306\20h\224b\10\23\204\270\60D\14C\210\320\4)B\220\222H(\10\0\361\12"
":\324\342\331'\314\340\2\11#(aB\63N\214\222\204\230\312\30\302\210S\316 \203\215 `\10\2"
"\206 `\10\342\11a\214\31\206\230d\240\370\200\4\23\326 C\15\62\10\0\361\13<\324\42\332'\314"
"\340\302\21J\10!B+N\220b\310\30b,a\2\42h\214\321\204\20\60\204\20C\10\61\4\361\204"
"\60\246\220\267\12\23R\210\60D\32\42\14\221F\30B\264\201\0\361\14\66\264f\332\307\264!\306\22F"
"\34\202\310\20N\4\1\207\224\344\200\42\10\21P\20b\30b\222\371\240\14\26\210 \341\210Q\204\60C"
"\20\61\214(\201\10\26fP\0\361\15\66\224f\332\307\264!\306\22F\34\202\310\20N\4\1\207\224d"
"\10\1\212`\214!F\30e\266\70\303\20\21\210 A\210\61D\21\302\214P\304\60\242\4\42XP\0"
"\361\16\67\265f\332\307\270!\6\23F \212\10'\204\200\42\4\31B\220C\206 \240\20\306\30r\304"
"\10C\231\17\316h\201\210\22\316\20E\14\63B\31\342\210\22f\240a\1\361\17+U\42\333\250<\61"
"\6\23F j\14'D\210D\16\31B\220!\10(\204\61\6!(>\60Q\13&\260A\306\32d"
"\24\0\361\20\61\224\346\332\307\264!\306\22F\34\202\310\20N\4\1\207\224\344\200\42\30c\210\10#\220"
" TaA\5\24T\70\3\15\63F \303\10\42\342\224\0\361\21\66\225\342\332\250\270\61\306\22G\34"
"\222\310\30N\4\21\211\234\242\10\342\211a\312)A\214\60\255\342\305\10'\14!\204\30e\10a\206\10"
"b(\61\204\20nL\221\0\361\22N\225\342\326\247\70\42F\42\207\24\21\4\12A\220\360\204\20\213\20"
"\241\204\20C\240A\206\20F\244 \212\10*\14\21J\12(\4\62\310\12\201\204\220\303\10'\214@\202"
"\20G\210@\202\30\42\210!\2\11B\14!\204\10$\230a\2\23\11\0\361\23+U\42\333\250<\61"
"\6\23F j\14'D\210D\16\31B\220!\10\30\206\61%\235(> \61\25H\224\2\211^ "
"\301\0\361\24\77\226\346\336\310\274!F\23F\240\202\10\31n\4!C\20\222\222A\10(\304\61\246\214"
"\200\302`d\5\30\306\30\301\204!\306\20\203\14!\320\10A\14\25J\60\341\10\62\212\60\202\214\42\6"
"\0\361\25>\225\346\332\250\270\61\306\32F\234\202\310\20n\4\21C\10S\232#\6!FX\202\230a"
"\226\71a\12\21Z\20\42\210\21L\10B\210\60D\60!\214\20F\24\304\10-\204!\302\23\12\0\361"
"\26>\225\346\332\310\264\61\306\32F\234\202\310\20O\4\21\307\224\346\210\42\214 \216\31H\230eN\230"
"B\204\26\204\20a\4\23D\20B\204\21D\30AD#\22a\4\21T\20a\4\30\24\0\361\27\64"
"\225\242\332\250\270\61\6\13G\234\202\310\30N\210\20\211\234\242\10\2\206aLI'\5\63L\30\303\211"
"\21\235\60\304\10b\30\261\302\21+\14\61\305\14\12\0\361\30/\225\346\336\250\270\61\306\22G\34\222\310"
"\20O\4\21\307\24s\310\20B\14\243\234\202\20\34\37$\61\3\11I\14)\211!\306h\201\206\5\0"
"\361\31\66\226\342\332\310\270\61H\42\250\214\341\206\10Q\4!))B\210B\210\21P\20c\34r\322"
"\371\0\206&F\20\302\10\22\206\20\302\210!\306pb\210\27jh\0\361\32\71\226\342\336\310\274\61D"
"\23F\240j\14\67D\220d\216\31B\220\42\210\30\206\61&\241\70x \302\210\21\304\30B\204!F"
"\20\303\14\21\206\20\303\14\21\306p\202\6\6\0\361\33=\225\242\332\250\270\61\6\13G\234\202\310\30N"
"\210\20\211\34\62\204\20E\20\60\14SL:p\354@\304\10\42\214 \306\20B\210\60\202\30'\214 "
"\204\30'\10!\2\23\42\214\0\303\2\361\34A\225\242\332\250\270\61\6\13G\234\202\310\30N\210\20\211"
"\34\62\204\20E\20\60\14SL:p\354@\304\10\42\214 \306\20B\210\60\202\30'\214 \204\30'"
"\10!\2\23\42\214@\2\21%\240 \0\361\35\70\324\346\331.\304\1\11\14A\314\300\212\20j\210!"
"H\21&\20!\310!!\210\301\206\10\60\4\1C\20\220<!L\71\303\20\203\16\24\37\220`\302\32"
"d\250AF\1\361\36=\324&\332-H\21G\14A\314\300\212\20J\14!J\221\210\20\344\20\42\232"
"\20\2\206\20b\10!\206 \236\20\246\230\42\302L\225\26NPb\204!\322\20A\214$\304\20\242\215"
"\4\0\361\37CU\346\332%\314!C\20P\210\0\303\20N\220\340B\21L\30\261\302\21&\14\201B"
"\31\42$AB\10\42\250\60D\20!\250\60\242\20\222\20b\20$D(!\10#F(A\10\42\15"
"\61\214)\1\0\361 -u\346\332\250\270\61\306\22G\34\222\310\20n\4\1E\20qH\22E\10P"
"\214r\12Bp|\240\203\11l\14\262\306 ,\230p\0\361!\61\225\242\332\250\270\61\6\13G\234\202"
"\310\30N\210\20\211\34\62\204 C\20\60\14cJB\217\264\220B\32g$\201\4\32#\214\321\206\34"
"\63(\0\361\42\77\227\242\336\251\300\61F\23G\244\202H\31N\14\21\205\20\62\10!\203\20Q\14\361"
"\306\70\345\230\21N %\230r\202\30\360\210\220\302\30\42\214\220\2\11f\214\60\306\12c\214\0\205\15"
"\13\0\361#A\327\42\336\251D\61\206\23F\244\202H\31N\14\21\205\20\62\10!\203\20Q\14\1\305"
"\70\345\230\21P\20&\230rB p\204q\2\32\42\214\60\306\10$\230!&\65\316X\201Dp\320"
"Q\303\2\361$\61\225\342\332\250<\61\6\23F j\14'D\210D\16\31B\220!\10\30\206\61%"
"\35\70>`\301\205\22BX\202\14\66\306`\203\204\20d\70\0\361%\65\225\346\332\250\270\61\4\23F"
" \212\214\66\202\200\42\4\71$\211A\4(\206)E\231\17Px\201\204\21V !\220\64\204\244\206"
"\10\242 \61\2\11/,\0\361&:\324\342\331'\314\340\2\11#(a\4\63N\14\242\204\230\312\30"
"\302\210S\14!\242\11!`\10\2\206 `\10\342\11a\214\31\206\230d\240\250\241\5\62\326\30c\15"
"\22\14\0\361'=\324\342\331&\314\360\302\10$(aB\63.\214\242\302\20c\224\61\204\11\210\34B"
"D\23B\300\20B\14!\304\20\304\23\302\30#\16\61\311@\361B\11\213\214\241\310\30*\204P\202\1"
"\361(\60\225\242\332\250<\61\6\23F\234\202\310\30N\210\20\211\234d\10\2\12a\214A\7\216\17\222"
"\30A\5\42FPb\4\42R \221\23\63,\0\361)\62\224\246\332\250\264\61\206\22F\34\202\310\20"
"N\4\1\207\224\344\200A\30cJ\10\310\21\35\222\30a\210\61\206\20c\204\61F\30\3\5\22\65!"
"\3\3\361*<\265\246\336\310\270!\6\23F j\14'\204\200\42\4\71d\10!\212 `\30\306\224"
"\204\36)\201\4G\302\60a\204!\304\20a\4\61D\70C\4\61\324\10b\4\26D\240\321\2\361+"
"\71\324\346\331.\304\1\11\14AL\261\214\10j\210!H\21F\14!\10*b\64!\2\14A@\352"
"\215`\214\31\206\30\23\302\21\201\215\32Z\30E\215\61\326 !\204\2\0\361,;\324\352\335-H\21"
"G\14A\314\300\312\10i\214!H\11F\20!\6\42D\64!\4\14!\304\20B\14A<\21\214\71"
"\302\24\203\316\33.\230\260\306 K\220\241\4\11!\34\0\361-Er\256\336(\70\21B\20\254\244@"
"\306\20E\220@\304 $\20\62F(\201\210\21\306\30B\230@\304\12%\254P\202\31a\214\21R("
"\201\214\61\4!A\204@\2!C\214AB\11\243\264\20B\20.$\0\361.#\226a\333\307\274!"
"F\23F$\202H\11P\210 \203\10\347\311 \4\24b\254 \4\61\246(\204\0\361/(U\346\332"
"\310\270\61\4\23F \212\10'\204\200\42\4\31B\220!\4\31\202\200B\30c\20\202\342\203\341>p"
"\356\0\361\60%\66\341\333\242\240\62\244\62\206\10\302\10!\314@\344\214$!\202\206\21B\30\21\304\30"
"d\14\61\12*\2\0\361\61\63r\256\336MDAD\21%DqB\32%\214P\302\22C\20\221D"
"\11i\60\22\202X$@Q\2\21c\264\21BIB\224\201\2\14H\24\301\210\1\361\62(\261\251\333"
"(\310\360\306\20%\214\61B\31G\24!\304\20D\22\242\210#\204H\203\204R\316\61\202\205\60\226\20"
"a\0\361\63+s\352\332\306\254\61F\22G\240\220\2\12\62H!\303\12\342\201\21B|@\204\340D"
"\10PD!C\24f\34\221R\33\10\0\361\64-sj\332g\270\263D\21I\240H\206%\304\3#"
"\204\370\200\10\341\14#\24\221B\10\361\300\10!>@\336\60!\5$\314P\307\0\361\65%\225\246\332"
"\250\270\61\306\22G\34\222\310\30m\4\21iQ\4\361\304\60\305\244\23\305\7\304a\367\1u\4\0\361"
"\66,\227\242\336\310\300!\206\23F(\202H\31n\214 \203\20\347\201 \303\10Q\14\61\2\12B\24"
"c\214B\37t\307\335\7\332\25\0\361\67<V\242\332C\60Q\306\32&\264\300I\42D\210Q\206\30"
"A\24\61\204\11!\34\21\304\31h$\201F\22G\4qH\21C\30\21\304\20e\14\61\12*;\264"
"`\306\32F\60\61\0\361\70DV\242\332C\60QF\21d\230PF\11.\64\222\10\21b\224!F"
"\20E\14aB\10G\4q\6\32I\240\221\304\21A\34R\304\20F\4\61D\31C\214\202\12\13."
"\224Q\202\31E\220a\4\23\3\0\361\71DV\242\332A<\61\306\20D\214\61\2\31b\220\240\2\11"
"\252\240\62\244\62\304\10\302\10!L\10\1\221\63\222\34\42\207\30!\204\21A\214A\306\20\243\240\242\2"
"\11*\220!\6\11c\14A\304\30B<!\0\361:)\227\242\336\310\300!\206\23F(\202H\31/"
"\220\240\232\14B\310\60\202iC\300P\214)\13I\361Ar\334}\240]\1\361;,\226\242\332\310\270"
"\61\6\23G \222\10\31N\10\221\232\14A\310 \202iB<Q\314\70\313|\240IX\231\5\362\301"
"$a\15\0\361<*\326b\332\7\61\226\334y \224\7\306x\240\210\7\212x\340\377\201\21\36\60\341"
"\201\42\36(\343\201Q\36\10\307%\326\14\2\361=:\366\42\332\310\70BF:&\234dBQ'\220"
"u\202h&\210fBp\347\3\301<\20\314\3\301\274\363N\10\355\204\300N\30\353\204\241N(\350\4"
"T\214P\243\210W\20\0\361>\61\326\42\332\310\270\62F:E\34tBI(\14\205\204X(\4\206"
"B`\251_\12a\241 T\12#!\61\22\12\5\235pN\21\212\220\321\14\2\361\77\61\326\42\332\7"
"\61BH\42H\234\222D)K\14\303\204(.\210\342\220;\320\6\217CN\204\342\202\60L\214\262D"
")I\34\202D\42e\60t\0\361@\60\326\42\332\7\61\62J\22J\34\301D\21N\14\1\205\10\62"
"\4!\307\34Tn\22)B\220A\10(Fx\202\210&\314P\42\15\63\30:\0\361A\60\326\42\332"
"\347\64BH\22\210\34\221J\21\253\14\301\214\10\256\4\341J\10\357\336C\256\210\340\212\20\314\214\300\12"
"\21\252\230\201H\32\205\60t\0\361B/\326b\332\7\261!N\22\4\31aR\11G\15\201\224\10I"
"\5\221T\10\212\237jI\211\220\224\20g\15aT\21%\35APZ\316 \0\361C<\327b\336\7"
"\65Q\212\12\6\235`T\11g\215p\330\10\247\211`\134\10\307\205p\36\10\347\201p\36\10\307\205p"
"\234\10\247\211p\332\10\206\225`\224\11'\35a\216\22\205\270\203\0\361DF\317\66\336d,\21\4\11"
"%\214@B\11#\14\62\42\22J\10\221\12!R!D*\204H\205\20\251\20\42\25B\224D\210\220"
"\20!\210\23F\20\302\4\61D(B\14\21\212\30\221\11'\30Q\304\21C\244r\0\361EC\317\66"
"\336d,\21\204\12#\250\60\302 #Ra\4\25FPa\4\25FPa\4\25FPa\4\25B"
"\224D\10A\34!\202\20&\210!B\21b\210P\204\30\42\230p\202\21E\34\61D*\7\0\361F"
"-\63\245\333+\34\61$\61\11\61\302\30b\10\61\242\20\4\11BHA\12D\204!\202\24\210\10\243"
"\4!F\20B\14\62D\10C\2\361\254,\326\42\332+\324\250\5\27\216X\2\5a\204X\250\251\305"
"\24+C\60\61\12S\334R\15-!\214\20H\254pD\13.\324H\1\361\255;\326\42\332&TQ"
"\202\23D\320\240\304(M\4\61F\23E\10\221\302))\324\60\14\32!\204\324Tz \220\7\310x"
"\240\204\7Lx\340\201&\36(\342\1B\36\20\313 \0\361\256\65\326\342\331+\324\250\5\27\214`\342"
"\210A\206`\307%\266\24W\206`b\24\246\320+\242,!H\20\215\224Q\304\20I\14A\302\12I"
"\250\240\310 \256\20\0\361\257\66\326\342\331+\324\250\5\27\214`\342\210A\206X\250\251\305\24W\206`"
"\201\24$\304*\62\14\343\6)\247$A\206\13#\304\60\202\10M\214\42\306(\210\204R\0\361\260\60"
"Vb\332\252\274\61\304\42\206\234!H\21&\10D\304QC\20\21T\20!\214\7\306x\240\204\7\376"
"\3%<P\306\3\303\210\200\202`\6\1\361\261\34\61.\333D\270\321\10\253\255\243\216B)\241e\36"
"\20\341\211\67ZY\310\30\0\361\262\65\226\42\332-P\61\207\24!\310 D\14B\304\60\304\33\203\250"
"\63\212IE\20'\302x@\220\7\310x\240\204\7Lx\300\210\7\310x\200\220\7\204:\10\0\361\263"
",\224&\332g@\362*g\271\323\20KK\251\7BZ\244\10\64\206\30\243\10R\206\30C$\21\4"
"\11+\34\241\2\42\203\260\262\0\361\264\60\267\42\336J\320\61\211\254\342\201\7\242\227\334b\17\210\63F"
"B\341\34e\210@\243\24C\230 \304\205!b A\204&H\21b\30\264\20\0\361\265:\365\342\331"
"'\320\360\2\11#,a\204+O\220\261\304\60\247\10\244\134y@\220\7\206x\200\210\7Jx\200\214"
"\7\306x@\30\307\310\7%\230\320B\21l\220\261\6\31\5\0\361\266:\324\42\332'\314\340\302\21J"
"\10!B+N\14c\310@+\241G\36\20\342\1\22\36 \341\1\22\36 \341\201\61\36\10\312\260\60"
"\205\10C\244!\302\20i\204!D\33\10\0\361\267/\264&\332\210\70\264\324y \214\7Fx\340\37"
"P\341\201\61\36\10\311|P\6\13D\220p\206(B\230\21\212\30F\224@\4\13\63(\0\361\270\60"
"\224&\332\210\70\264\324y \214\7Fx\340\37P\341\201\61\36\10\311lq\206!\42\20A\202\20c"
"\210\42\204\31\241\210aD\11D\260\240\0\361\271\65\265&\332\250\70\304\24z \220\7Hx\240\204\7"
"Jx\340\201\25\36(\342\201A\36\10\312|pF\13D\224p\206(b\230\21\312\20G\224\60\3\15"
"\13\0\361\272-u\342\332\250<\304\24z \220\7\206x\240\204\7\36X\341\201\22\36(\342\201A\36"
"\10\312|\220\203\11L\24\301\6\31K\24a\0\361\273+\224\246\326\247\270\303\22z\344\1!\36 \341"
"\201\77@\304\3c<U>\10!\5$\222\70c\204\61\216 \201\10\66\342H\0\361\274\61\225\342\332"
"\250\270\304\22z@\214\7\210x\340\17\240\360\0\31\17\210t\340\370\0\210\21N\30B\10\61\312\20\302"
"\14\21\304Pb\10!\334\230\42\1\361\275=\225\242\332\210\274\323Rz\345\1A\36\30\242\21!\226("
"\4\211$PX\303\204uFHo\354\60\302\11#\220 \304\21\42\220 \206\10b\210@\202\20C\10"
"!\2\11f\230\260F\2\361\276*U\42\333\250<\304\24z \214\7\210x\340\7Dx\240\204\7\310"
"x@\244\363\301\15$Ra\204\22R \321\13$\30\0\361\277<\226\346\336\347\70\225\332y\200\214\7"
"Jx\300\204\7~\240\210\7\12y ,\243B\15$\230@\302\30#\230\60\304\30b\220!\4\32!"
"\210\221D\21%\34AF\21&\224Q\2\1\361\300:\225\246\332\250<\264\326y \220\7Hx\240\204"
"\7\376\0\22\17\220\361\200P\346\204)DhA\210 F\60!\10!\302\20\301\204\60B\30Q\20#"
"\210\240B\30\42<\241\0\361\301:\225\246\332\250<\264\326y \220\7Hx\240\204\7\376@\12\17\220"
"\361\200P\346\204)DhA\10\21F\60A\4!D\30A\204\21D\64\42\21F\20A\5\21F\200"
"A\1\361\302\62\225f\332\250\270\304\24z \214\7\210x\340\17\240\360\0\21\17\20\362J(\245\4\22"
"\236\20b\204\23\206\30\321\21\42\234Q\306\12#zc\6\5\0\361\303\60\265\246\336\250\270\264\326y@"
"\214\7Hx\240\204\7\376@\12\17\220\361\200H'\206\17\224\30!\205\22HHb\210\61\220\30b\214"
"\26hX\0\361\304\60\226\242\332\250\274\324\24z@\220\7\212x\240\204\7~\300\204\7\312x\200\220\7"
"\302\62\37\240p\4\13$\10a\304\220\224\30b\14\27jh\0\361\305\66\266\242\336\251<\324Tz "
"\220\7\212x\340\17\260\360@\21\17\24\362@Xf\207\32\210\20a\210\21\304\30\42\14\21\206\20\303\14"
"\21\306XA\210!\236\240\201\1\361\306\65\225b\332\250<\304Rz \214\7\210x\340\17\240\360\0\31"
"\17\210t\242\330\201\10!D\30A\214!\204\20a\4\61N\20\62\24\204\20\201\11\21F\200a\1\361"
"\307=\265\242\332\250<\304Rz\344\1\42\36\370\3(<P\302\3\204<\20\322\311\201\6\42\204\20a"
"\4\61\206\20B\4!\304\70AHK\10!\2\11D\4!\302\10$\20Q\2\12\2\0\361\310\66\324"
"&\332M\304\1\11\14A\300 \304*C$\64\306pA\204\7\206x`\204\7Hx\340\201$\36\30"
"\342\201\61\236*\37\214`\302\22E\254A\206\22E\30\0\361\311:\324&\332-H\21G\14A\314\300"
"\212\20\352\210R\22\21\342\205 \36\20\342\1\22\36 \341\1\22\36 \342\1A^:,\234\240\304\10"
"C\244!\202\30I\210\21F\33\11\0\361\312+u\342\326&L\61G$\261\274\323PKK\255U\2"
"Yd\10F\206h\242\204&J`\342\204%NP\343\10T\252\63\4\0\361\313'u\346\336\250\270\264"
"\326y@\214\7\210x\340\17\240\360@\11\17\20\362V\371 \7\23\330\30d\215AX\60\341\0\361\314"
",\225\242\332\250<\304\24z \214\7\210x\340\7Dx\240\204\7\310x@\244\363\201\10)$b\6"
"\32g\244@\42\67\344\230A\1\361\315<\267b\336\251D\344\224z \224\7\10y\240\210\7\214x\300"
"\210\7\214x\300\214\7\210y \224`\214\11\201\300\21\6\34\42\214\220\2\11f\234\241\306\10c\254\60"
"\306\10p\324\260\0\361\316:\327\42\336\251D\344\224z \224\7\10y\240\210\7\214x\300\210\7\214x"
"\300\214\7\210y&\230rB \360\34q\206\10#\220\230\31C\210\241\306\31+\220\10\16:jX\0"
"\361\317+u\242\332\250<\304\24z \220\7\206x\240\204\7~@\204\7Jx\200\220\7\202\62\37\344"
"P\202\13\204\254\61\6\23%\34\0\361\320\63\225\246\332\250\270\264\226y`\214\7Hx\240\204\7~@"
"\210\7\310x@\244\363\301\11/\220\60\302\12$\204\202\206\220\324\20A\24$F \341\205\5\0\361\321"
"\67\324\42\332'\314\340\2\11#(aD+N\20\261\304\60\246\10\224\34y@\210\7Hx\200\204\7"
"Hx\200\210\7\206x@$\3E\15-\220\261\306\30k\220`\0\361\322:\364\342\331&\314\360\302\10"
"$(aD+N\214\261\204\70\206\210\224\34y@\210\7Hx\200\204\7Hx\200\204\7\210x@\24"
"\267\310\7B\224\260\310\30\212\214\301B\11\6\0\361\323\60\265\242\332\250<\304\24z \214\7\210x\340"
"\17\240\360@\11\17\20\362@P\346\203\33\210\30!\215\21\306@c\204\61R\30\202\4'fX\0\361"
"\324\65\265\246\336\250\70\304\24z \220\7Hx\240\204\7\376@\12\17\220\361\200\60! \21\30\321\203"
"\4\42F\30\323\10c\214@\306\10c\244\60\4\11N\314\320\0\361\325:\225f\336\211<\304\24z "
"\220\7\206x\240\204\7\36X\341\201\22\36(\341\1B\36\10\312\344\60\206\11#\210\311\4\61\2\21A"
"\14\21\304\20\341\214 F`A\4\32\26\0\361\326:\325&\332M\310\21I\14A\304 D#C("
"$JQC\214\7B\10\342\201\61\36 \341\201\22\36(\341\201\42\36\30\344\201\240\314\7$\224\340\302"
"(k\210\262\6\11\10\0\361\327\70\324*\336-H\21G\14A\300 \2+#\244$\210x\344\1!"
"\36 \341\1\22\36 \341\1\22\36 \342\1Q\202\70\42,\362\301\10&\254A\206\32\203\254`\302\1"
"\361\330<r\256\336(\70\21B\20\254\244@\306\20E\220@\304 $\20\62F(\201\210\21\222\20\346"
"\254\313\10\221B\12%\220\61\15\22D\10$\20\62\304\30$\224@H\13!\4\341B\2\361\331\36\264"
"%\333\210\70\264\324y \214\7Fx\340\3\203=\360\200\12\17\214\361@H\346\0\361\332!U\342\332"
"\347\264\264\326y@\214\7Hx\340\17\244\360\0\31\17\210tb\370\240\270\17\234\63\0\361\333\33V\241"
"\333D\254`\254\201\10\12J<\360\7BHB\205c\16!\250\10\0\361\334.\64\352\336\42\250aD"
"\21d\310\201\2\62%\14\61\310+D\240R\302\61+\11\66\6$&\20!H\63G\11\221\204\31)"
"\220\242\220\1\361\335-\364\345\332(\214\360B\20a\70\61\306\21C\220QF\42E\4a\310\10%\224"
"\62\202*\315\254C\330\25N\210\320B\30N\210p\0\361\336&q\256\336\345 u\204\31E Q\304"
"\23oJ!<\360\0AE\4F\236\200\342ID Q\10!\7\21\0\361\337.\221\252\326\344 u"
"\204\31E Q\304\23+\210\7Bx\340\201Q\210\11\250<\22\202x \204\7\202\23O\20\201D!"
"\204\30\245\310\1\361\340\42\225\246\336\347\264\224\134y`\214\7Hx\340\17\244\360\0\31\17\210t\242\370"
"\200\70\354>\240\216\0\361\341&\265f\336\250\270\264\326y@\214\7\210x\340\3\244\241\366\200\11\17\220"
"\361\200H\7\212\17\212\303\356\3\352\10\0\361\342,V\242\332C\60a\306\32&\264\320\305\12\306\230#"
"\322@a\205\7~\240\4\25\322H\1\225CH\42<\264`\306\32E\60\61\0\361\343\64V\242\332C"
"\60a\6\21e\230PF\11.\70\261\202\61\346\210\64PX\341\201\37(A\205\64R@\345\20\222H"
"\13.\224Q\202\31D\224Q\4\23\3\0\361\344;V\242\332B\70\61\306\20D\214\61\2\31b\220\240"
"\2\11L$R\214\71\2\215\7Jx\340\7JX\1\215$\216\61\205\244\261\2\11*\220!\6\11c"
"\14A\304\30C\70!\0\361\345$\225\342\332\7\61\205DP\347\1\61\36 B\251\7\36`\341\270\363"
"\36\30\10\275\361Aq\330\35\347\335\0\361\346'\225\242\332\7\61\205\232y`\214\7HX\352\201\7X"
"\70\356\274\7D:\61|PHX\227\5\362\1$a\11\0\361\347\67\316\66\332\203$C\302\60$\14"
"#\210\60$\14!\4\22B !\4\22B !\4\22B !\4\22B\234!F!\202\220\61\10"
"\31\203\20\22\12\321\12B\344\0\361\350%\316\66\332c\250\222\14\62\202\10\203\354\207\204\20g\210Q\210"
" d\14B\306 \204\204B\24I\6!r\0\361\351=\323%\327\341\1\21\36x@\205\21H\20\242"
"\10\21H\20\242\10\21\206\20\242\204(\214 B\20$\204\61\202\24H\20B\42$\10!\202\20B\214"
"\60\202$Fx\340\201\24\36\20\1\0\0";
#endif /* U8G2_USE_LARGE_FONTS */

103
src/weather.cpp Normal file
View File

@@ -0,0 +1,103 @@
#include "weather.h"
#include <_preference.h>
TaskHandle_t WEATHER_HANDLER;
String _qweather_host;
String _qweather_key;
String _qweather_loc;
int8_t _weather_status = -1;
int8_t _weather_type = -1;
Weather _weather_now = {};
DailyWeather dailyWeather = {};
DailyForecast _daily_forecast = {
.weather = &dailyWeather,
.length = 1
};
int8_t weather_type() {
return _weather_type;
}
int8_t weather_status() {
return _weather_status;
}
Weather* weather_data_now() {
return &_weather_now;
}
DailyForecast* weather_data_daily() {
return &_daily_forecast;
}
void task_weather(void* param) {
Serial.println("[Task] get weather begin...");
Preferences pref;
pref.begin(PREF_NAMESPACE);
_weather_type = pref.getString(PREF_QWEATHER_TYPE).compareTo("1") == 0 ? 1 : 0;
pref.end();
Serial.printf("Weather Type: %d\n", _weather_type);
API<> api;
// 实时天气
bool success;
if (_weather_type == 0) {
success = api.getForecastDaily(_daily_forecast, _qweather_host.c_str(), _qweather_key.c_str(), _qweather_loc.c_str());
} else {
success = api.getWeatherNow(_weather_now, _qweather_host.c_str(), _qweather_key.c_str(), _qweather_loc.c_str());
}
if (success) {
_weather_status = 1;
} else {
_weather_status = 2;
}
Serial.println("[Task] get weather end...");
WEATHER_HANDLER = NULL;
vTaskDelete(NULL);
}
void weather_exec(int status) {
_weather_status = status;
if (status > 0) {
return;
}
if (!WiFi.isConnected()) {
_weather_status = 2;
return;
}
// Preference 获取配置信息。
Preferences pref;
pref.begin(PREF_NAMESPACE);
_qweather_host = pref.getString(PREF_QWEATHER_HOST, "api.qweather.com");
_qweather_key = pref.getString(PREF_QWEATHER_KEY, "");
_qweather_loc = pref.getString(PREF_QWEATHER_LOC, "");
pref.end();
if (_qweather_key.length() == 0 || _qweather_loc.length() == 0) {
Serial.println("Qweather key/locationID invalid.");
_weather_status = 3;
return;
}
if (WEATHER_HANDLER != NULL) {
vTaskDelete(WEATHER_HANDLER);
WEATHER_HANDLER = NULL;
}
xTaskCreate(task_weather, "WeatherData", 1024 * 8, NULL, 2, &WEATHER_HANDLER);
}
void weather_stop() {
if (WEATHER_HANDLER != NULL) {
vTaskDelete(WEATHER_HANDLER);
WEATHER_HANDLER = NULL;
}
_weather_status = 2;
}

125
src/web_server.txt Normal file
View File

@@ -0,0 +1,125 @@
#include <WiFi.h>
#include <WebServer.h>
// WiFi配置
const char* ap_ssid = "ESP32_Config";
const char* ap_password = "12345678";
// Web Server实例
WebServer server(80);
// 存储配置
String wifi_ssid = "";
String wifi_password = "";
int timezone = 8;
void setup() {
Serial.begin(115200);
// 启动AP模式
WiFi.softAP(ap_ssid, ap_password);
Serial.println("AP模式已启动");
Serial.print("IP地址: ");
Serial.println(WiFi.softAPIP());
// 配置路由
server.on("/", handleRoot);
server.on("/save", handleSave);
// 启动Web Server
server.begin();
Serial.println("HTTP服务器已启动");
}
void loop() {
server.handleClient();
}
// 处理根路径请求
void handleRoot() {
// 读取HTML文件内容
String html = R"=====(
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设备配置</title>
<style>
/* 样式内容 */
</style>
</head>
<body>
<div class="container">
<h1>设备配置</h1>
<form id="config-form" action="/save" method="POST">
<div class="form-group">
<label for="wifi-ssid">WiFi SSID</label>
<input type="text" id="wifi-ssid" name="wifi-ssid" required>
</div>
<div class="form-group">
<label for="wifi-password">WiFi 密码</label>
<input type="password" id="wifi-password" name="wifi-password">
</div>
<div class="form-group">
<label for="timezone">时区</label>
<select id="timezone" name="timezone">
<option value="8">GMT+8 中国标准时间</option>
<option value="0">GMT 格林威治时间</option>
<option value="-5">GMT-5 美国东部时间</option>
</select>
</div>
<button type="submit">保存配置</button>
</form>
</div>
<script>
// JavaScript内容
</script>
</body>
</html>
)=====";
server.send(200, "text/html", html);
}
// 处理保存配置请求
void handleSave() {
if (server.method() == HTTP_POST) {
// 获取表单数据
wifi_ssid = server.arg("wifi-ssid");
wifi_password = server.arg("wifi-password");
timezone = server.arg("timezone").toInt();
// 保存配置到EEPROM可选
// saveConfigToEEPROM();
// 返回成功响应
server.send(200, "text/plain", "配置保存成功");
// 尝试连接新配置的WiFi
connectToWiFi();
}
}
// 连接WiFi
void connectToWiFi() {
if (wifi_ssid.length() > 0) {
Serial.println("正在连接WiFi...");
WiFi.begin(wifi_ssid.c_str(), wifi_password.c_str());
int retry = 0;
while (WiFi.status() != WL_CONNECTED && retry < 10) {
delay(1000);
Serial.print(".");
retry++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi连接成功");
Serial.print("IP地址: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\nWiFi连接失败");
}
}
}

11
test/README Normal file
View File

@@ -0,0 +1,11 @@
This directory is intended for PlatformIO Test Runner and project tests.
Unit Testing is a software testing method by which individual units of
source code, sets of one or more MCU program modules together with associated
control data, usage procedures, and operating procedures, are tested to
determine whether they are fit for use. Unit testing finds problems early
in the development cycle.
More information about PlatformIO Unit Testing:
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html