diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0d27ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +/firmware.map +/mklink.bat +/output diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7c486f1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,67 @@ +# Continuous Integration (CI) is the practice, in software +# engineering, of merging all developer working copies with a shared mainline +# several times a day < https://docs.platformio.org/page/ci/index.html > +# +# Documentation: +# +# * Travis CI Embedded Builds with PlatformIO +# < https://docs.travis-ci.com/user/integration/platformio/ > +# +# * PlatformIO integration with Travis CI +# < https://docs.platformio.org/page/ci/travis.html > +# +# * User Guide for `platformio ci` command +# < https://docs.platformio.org/page/userguide/cmd_ci.html > +# +# +# Please choose one of the following templates (proposed below) and uncomment +# it (remove "# " before each line) or use own configuration according to the +# Travis CI documentation (see above). +# + + +# +# Template #1: General project. Test it using existing `platformio.ini`. +# + +# language: python +# python: +# - "2.7" +# +# sudo: false +# cache: +# directories: +# - "~/.platformio" +# +# install: +# - pip install -U platformio +# - platformio update +# +# script: +# - platformio run + + +# +# Template #2: The project is intended to be used as a library with examples. +# + +# language: python +# python: +# - "2.7" +# +# sudo: false +# cache: +# directories: +# - "~/.platformio" +# +# env: +# - PLATFORMIO_CI_SRC=path/to/test/file.c +# - PLATFORMIO_CI_SRC=examples/file.ino +# - PLATFORMIO_CI_SRC=path/to/test/directory +# +# install: +# - pip install -U platformio +# - platformio update +# +# script: +# - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..e80666b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9f0d199 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.associations": { + "functional": "cpp", + "cmath": "cpp", + "string": "cpp", + "*.tcc": "cpp", + "sstream": "cpp" + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + 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 +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git "a/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/BOM.xlsx" "b/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/BOM.xlsx" new file mode 100644 index 0000000..9b36bf0 Binary files /dev/null and "b/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/BOM.xlsx" differ diff --git "a/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_2020-01-15.zip" "b/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_2020-01-15.zip" new file mode 100644 index 0000000..40f5213 Binary files /dev/null and "b/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_2020-01-15.zip" differ diff --git "a/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\345\217\215\351\235\242.png" "b/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\345\217\215\351\235\242.png" new file mode 100644 index 0000000..b5d9a4b Binary files /dev/null and "b/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\345\217\215\351\235\242.png" differ diff --git "a/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\346\255\243\351\235\242.png" "b/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\346\255\243\351\235\242.png" new file mode 100644 index 0000000..5111a14 Binary files /dev/null and "b/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\346\255\243\351\235\242.png" differ diff --git "a/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/\345\216\237\347\220\206\345\233\276.png" "b/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/\345\216\237\347\220\206\345\233\276.png" new file mode 100644 index 0000000..32cd189 Binary files /dev/null and "b/file/1\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/\345\216\237\347\220\206\345\233\276.png" differ diff --git "a/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/BOM.xlsx" "b/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/BOM.xlsx" new file mode 100644 index 0000000..cf34167 Binary files /dev/null and "b/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/BOM.xlsx" differ diff --git "a/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_2020-01-14.zip" "b/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_2020-01-14.zip" new file mode 100644 index 0000000..59d9bb0 Binary files /dev/null and "b/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_2020-01-14.zip" differ diff --git "a/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\345\217\215\351\235\242.png" "b/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\345\217\215\351\235\242.png" new file mode 100644 index 0000000..37916ef Binary files /dev/null and "b/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\345\217\215\351\235\242.png" differ diff --git "a/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\346\255\243\351\235\242.png" "b/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\346\255\243\351\235\242.png" new file mode 100644 index 0000000..7985925 Binary files /dev/null and "b/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\346\255\243\351\235\242.png" differ diff --git "a/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/\345\216\237\347\220\206\345\233\276.png" "b/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/\345\216\237\347\220\206\345\233\276.png" new file mode 100644 index 0000000..109e256 Binary files /dev/null and "b/file/2\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/\345\216\237\347\220\206\345\233\276.png" differ diff --git "a/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/BOM.xlsx" "b/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/BOM.xlsx" new file mode 100644 index 0000000..28550cb Binary files /dev/null and "b/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/BOM.xlsx" differ diff --git "a/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_2020-01-14.zip" "b/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_2020-01-14.zip" new file mode 100644 index 0000000..295b232 Binary files /dev/null and "b/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_2020-01-14.zip" differ diff --git "a/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\345\217\215\351\235\242.png" "b/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\345\217\215\351\235\242.png" new file mode 100644 index 0000000..bd2fdb2 Binary files /dev/null and "b/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\345\217\215\351\235\242.png" differ diff --git "a/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\346\255\243\351\235\242.png" "b/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\346\255\243\351\235\242.png" new file mode 100644 index 0000000..fc2c1c0 Binary files /dev/null and "b/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/PCB_\346\255\243\351\235\242.png" differ diff --git "a/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/\345\216\237\347\220\206\345\233\276.png" "b/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/\345\216\237\347\220\206\345\233\276.png" new file mode 100644 index 0000000..3cf05e2 Binary files /dev/null and "b/file/3\350\267\257\345\274\200\345\205\263\346\250\241\345\235\227/\345\216\237\347\220\206\345\233\276.png" differ diff --git "a/file/images/LED\347\212\266\346\200\201.png" "b/file/images/LED\347\212\266\346\200\201.png" new file mode 100644 index 0000000..17a5200 Binary files /dev/null and "b/file/images/LED\347\212\266\346\200\201.png" differ diff --git a/file/images/tab1.png b/file/images/tab1.png new file mode 100644 index 0000000..918a8d4 Binary files /dev/null and b/file/images/tab1.png differ diff --git a/file/images/tab2.png b/file/images/tab2.png new file mode 100644 index 0000000..111e3cf Binary files /dev/null and b/file/images/tab2.png differ diff --git a/file/images/tab3.png b/file/images/tab3.png new file mode 100644 index 0000000..b61048d Binary files /dev/null and b/file/images/tab3.png differ diff --git a/file/images/tab4.png b/file/images/tab4.png new file mode 100644 index 0000000..e273e41 Binary files /dev/null and b/file/images/tab4.png differ diff --git "a/file/images/\345\217\215\351\235\242.png" "b/file/images/\345\217\215\351\235\242.png" new file mode 100644 index 0000000..f13e2ec Binary files /dev/null and "b/file/images/\345\217\215\351\235\242.png" differ diff --git "a/file/images/\346\210\220\345\223\201.png" "b/file/images/\346\210\220\345\223\201.png" new file mode 100644 index 0000000..eb1e245 Binary files /dev/null and "b/file/images/\346\210\220\345\223\201.png" differ diff --git "a/file/images/\346\255\243\351\235\242.png" "b/file/images/\346\255\243\351\235\242.png" new file mode 100644 index 0000000..06c2c33 Binary files /dev/null and "b/file/images/\346\255\243\351\235\242.png" differ diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -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 diff --git a/include/RadioReceive.h b/include/RadioReceive.h new file mode 100644 index 0000000..bc56681 --- /dev/null +++ b/include/RadioReceive.h @@ -0,0 +1,29 @@ +#ifdef USE_RCSWITCH + +// RadioReceive.h +#ifndef _RADIORECEIVE_h +#define _RADIORECEIVE_h + +#include "Relay.h" +#include + +class RadioReceive +{ +protected: + RCSwitch *mySwitch; + unsigned long lastVaue = 0; + int lastTime = 0; + int studyTime = 0; + Relay *relay; + +public: + uint8_t studyCH = 0; + void init(Relay *_relay, uint8_t io); + void study(uint8_t ch); + void del(uint8_t ch); + void delAll(); + void loop(); +}; + +#endif +#endif \ No newline at end of file diff --git a/include/Relay.h b/include/Relay.h new file mode 100644 index 0000000..a2f49bc --- /dev/null +++ b/include/Relay.h @@ -0,0 +1,105 @@ +// Relay.h +#ifndef _RELAY_h +#define _RELAY_h + +#include "Module.h" +#include "RelayConfig.pb.h" +#include "Template.h" + +#define MODULE_CFG_VERSION 1001 //1001 - 1500 + +#define MAX_STUDY_RECEIVER_NUM 10 // 遥控最大学习数 + +const char HASS_DISCOVER_RELAY[] PROGMEM = + "{\"name\":\"%s_%d\"," + "\"cmd_t\":\"%s\"," + "\"stat_t\":\"%s\"," + "\"pl_off\":\"OFF\"," + "\"pl_on\":\"ON\"," + "\"avty_t\":\"%s\"," + "\"pl_avail\":\"online\"," + "\"pl_not_avail\":\"offline\"}"; + +enum LightColor +{ + None, + WhiteColor, + YellowColor, + YellowWhiteColor +}; + +#ifdef USE_RCSWITCH +class RadioReceive; +#endif +class RelayButton; +class Relay : public Module +{ +private: + uint8_t GPIO_PIN[MAX_GPIO_PIN - MIN_FLASH_PINS]; + uint8_t operationFlag = 0; // 0每秒 + + char powerStatTopic[80]; + RelayButton *btns; + + // PWM + Ticker *ledTicker; + int ledLevel = 0; + int ledLight = 2023; + bool ledUp = true; + bool canLed = true; + void led(uint8_t ch, bool isOn); + void ledPWM(uint8_t ch, bool isOn); + void ledTickerHandle(); + bool checkCanLed(bool re = false); + + void httpDo(ESP8266WebServer *server); + void httpSetting(ESP8266WebServer *server); + void httpHa(ESP8266WebServer *server); +#ifdef USE_RCSWITCH + void httpRadioReceive(ESP8266WebServer *server); +#endif + + void loadModule(uint8_t module); + void reportPower(); + +#ifdef USE_TRICOLOR + void httpDownlightSetting(ESP8266WebServer *server); + unsigned long colorOffTime = 0; + void colorOnOff(uint8_t ch, bool isOn); +#endif + +public: + RelayConfigMessage config; + uint8_t lastState = 0; + uint8_t channels = 0; + +#ifdef USE_RCSWITCH + RadioReceive *radioReceive = NULL; +#endif + + void init(); + String getModuleName() { return F("relay"); } + String getModuleCNName(); + String getModuleVersion() { return F("2020.02.26.2100"); } + String getModuleAuthor() { return F("情留メ蚊子"); } + bool moduleLed(); + + void loop(); + void perSecondDo(); + + void readConfig(); + void resetConfig(); + void saveConfig(bool isEverySecond); + + void mqttCallback(String topicStr, String str); + void mqttConnected(); + void mqttDiscovery(bool isEnable = true); + + void httpAdd(ESP8266WebServer *server); + void httpHtml(ESP8266WebServer *server); + String httpGetStatus(ESP8266WebServer *server); + + void switchRelay(uint8_t ch, bool isOn, bool isSave = true); +}; + +#endif diff --git a/include/RelayButton.h b/include/RelayButton.h new file mode 100644 index 0000000..6886c04 --- /dev/null +++ b/include/RelayButton.h @@ -0,0 +1,41 @@ +// RelayButton.h +#ifndef _RELAYBUTTON_h +#define _RELAYBUTTON_h + +#include "Arduino.h" + +static const uint8_t DEBOUNCED_STATE = 0b00000001; +static const uint8_t UNSTABLE_STATE = 0b00000010; +//static const uint8_t CHANGED_STATE = 0b00000100; + +class RelayButton +{ +protected: + uint8_t ch = -1; + uint8_t io = -1; + // 按键 + int debounceTime = 50; + unsigned long timingStart = 0; + unsigned long intervalStart; + int switchCount = 0; + bool currentState; + + // 等待开关再次切换的时间(以毫秒为单位)。 + // 300对我来说效果很好,几乎没有引起注意。 如果您不想使用此功能,请设置为0。 + unsigned long specialFunctionTimeout = 300; + int lastTime = 0; + + Relay *relay; + + uint8_t stateFlag; + inline void setStateFlag(const uint8_t flag) { stateFlag |= flag; } + inline void unsetStateFlag(const uint8_t flag) { stateFlag &= ~flag; } + inline void toggleStateFlag(const uint8_t flag) { stateFlag ^= flag; } + inline bool getStateFlag(const uint8_t flag) { return ((stateFlag & flag) != 0); } + +public: + void init(Relay *_relay, uint8_t _ch, uint8_t _io); + void loop(); +}; + +#endif diff --git a/include/RelayConfig.pb.h b/include/RelayConfig.pb.h new file mode 100644 index 0000000..40b53af --- /dev/null +++ b/include/RelayConfig.pb.h @@ -0,0 +1,83 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.3.9.4 at Thu Feb 20 15:26:50 2020. */ + +#ifndef PB_RELAYCONFIG_PB_H_INCLUDED +#define PB_RELAYCONFIG_PB_H_INCLUDED +#include + +/* @@protoc_insertion_point(includes) */ +#if PB_PROTO_HEADER_VERSION != 30 +#error Regenerate this file with the current version of nanopb generator. +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* Struct definitions */ +typedef struct _RelayConfigMessage { + uint8_t led_type; + uint16_t led_start; + uint16_t led_end; + uint8_t power_on_state; + uint8_t last_state; + uint32_t study_index[4]; + uint32_t study[40]; + uint8_t led_light; + uint8_t led_time; + uint8_t downlight_ch; + uint8_t downlight_index; + uint8_t downlight_color[3]; + uint8_t downlight_default; + uint16_t downlight_interval; + uint8_t power_mode; + uint8_t module_type; + uint16_t report_interval; +/* @@protoc_insertion_point(struct:RelayConfigMessage) */ +} RelayConfigMessage; + +/* Default values for struct fields */ + +/* Initializer values for message structs */ +#define RelayConfigMessage_init_default {0, 0, 0, 0, 0, {0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, 0, 0, 0, {0, 0, 0}, 0, 0, 0, 0, 0} +#define RelayConfigMessage_init_zero {0, 0, 0, 0, 0, {0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, 0, 0, 0, {0, 0, 0}, 0, 0, 0, 0, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define RelayConfigMessage_led_type_tag 1 +#define RelayConfigMessage_led_start_tag 2 +#define RelayConfigMessage_led_end_tag 3 +#define RelayConfigMessage_power_on_state_tag 4 +#define RelayConfigMessage_last_state_tag 5 +#define RelayConfigMessage_study_index_tag 6 +#define RelayConfigMessage_study_tag 7 +#define RelayConfigMessage_led_light_tag 8 +#define RelayConfigMessage_led_time_tag 9 +#define RelayConfigMessage_downlight_ch_tag 10 +#define RelayConfigMessage_downlight_index_tag 11 +#define RelayConfigMessage_downlight_color_tag 12 +#define RelayConfigMessage_downlight_default_tag 13 +#define RelayConfigMessage_downlight_interval_tag 14 +#define RelayConfigMessage_power_mode_tag 19 +#define RelayConfigMessage_module_type_tag 20 +#define RelayConfigMessage_report_interval_tag 21 + +/* Struct field encoding specification for nanopb */ +extern const pb_field_t RelayConfigMessage_fields[18]; + +/* Maximum encoded size of messages (where known) */ +#define RelayConfigMessage_size 369 + +/* Message IDs (where set with "msgid" option) */ +#ifdef PB_MSGID + +#define RELAYCONFIG_MESSAGES \ + + +#endif + +#ifdef __cplusplus +} /* extern "C" */ +#endif +/* @@protoc_insertion_point(eof) */ + +#endif diff --git a/include/Template.h b/include/Template.h new file mode 100644 index 0000000..4c39b5b --- /dev/null +++ b/include/Template.h @@ -0,0 +1,149 @@ +#ifndef _TEMPLATE_h +#define _TEMPLATE_h + +#include "Arduino.h" + +#define MAX_GPIO_PIN 17 // Number of supported GPIO +#define MIN_FLASH_PINS 4 // Number of flash chip pins unusable for configuration (GPIO6, 7, 8 and 11) + +typedef struct MYTMPLT +{ + char name[15]; + uint8_t io[MAX_GPIO_PIN - MIN_FLASH_PINS]; +} mytmplt; + +enum UserSelectablePins +{ + GPIO_NONE, // Not used + GPIO_REL1, // Relays + GPIO_REL2, + GPIO_REL3, + GPIO_REL4, + GPIO_KEY1, // Button usually connected to GPIO0 + GPIO_KEY2, + GPIO_KEY3, + GPIO_KEY4, + GPIO_LED_POWER, // Power Led (1 = On, 0 = Off) + GPIO_LED_POWER_INV, // Power Led (0 = On, 1 = Off) + GPIO_LED1, // Leds + GPIO_LED2, + GPIO_LED3, + GPIO_LED4, + GPIO_RFRECV, // RF receiver +}; + +enum SupportedModules +{ + SONOFF_BASIC, + CH1, + CH2, + CH3, + iciness_CH3, + + END // 占位 +}; + +const mytmplt Modules[] PROGMEM = { + { + "Sonoff Basic", // Sonoff Basic (ESP8266) + GPIO_KEY1, // GPIO00 Button + GPIO_NONE, // GPIO01 + GPIO_NONE, // GPIO02 + GPIO_NONE, // GPIO03 + GPIO_NONE, // GPIO04 + 0, // GPIO05 + // GPIO06 (SD_CLK Flash) + // GPIO07 (SD_DATA0 Flash QIO/DIO/DOUT) + // GPIO08 (SD_DATA1 Flash QIO/DIO/DOUT) + 0, // GPIO09 (SD_DATA2 Flash QIO or ESP8285) + 0, // GPIO10 (SD_DATA3 Flash QIO or ESP8285) + // GPIO11 (SD_CMD Flash) + GPIO_REL1, // GPIO12 Red Led and Relay (0 = Off, 1 = On) + GPIO_LED_POWER_INV, // GPIO13 Green Led (0 = On, 1 = Off) - Link and Power status + GPIO_NONE, // GPIO14 + 0, // GPIO15 + 0 // GPIO16 + }, + { + "1 Channel", // 3 Channel (ESP8285) + GPIO_LED1, // GPIO00 + GPIO_NONE, // GPIO01 + GPIO_NONE, // GPIO02 + GPIO_NONE, // GPIO03 + GPIO_KEY1, // GPIO04 + GPIO_NONE, // GPIO05 + // GPIO06 (SD_CLK Flash) + // GPIO07 (SD_DATA0 Flash QIO/DIO/DOUT) + // GPIO08 (SD_DATA1 Flash QIO/DIO/DOUT) + GPIO_NONE, // GPIO09 (SD_DATA2 Flash QIO or ESP8285) + GPIO_NONE, // GPIO10 (SD_DATA3 Flash QIO or ESP8285) + // GPIO11 (SD_CMD Flash) + GPIO_NONE, // GPIO12 + GPIO_RFRECV, // GPIO13 + GPIO_REL1, // GPIO14 + GPIO_NONE, // GPIO15 + GPIO_LED_POWER_INV // GPIO16 Led (1 = On, 0 = Off) - Link and Power status + }, + { + "2 Channel", // 3 Channel (ESP8285) + GPIO_LED1, // GPIO00 + GPIO_NONE, // GPIO01 + GPIO_LED2, // GPIO02 + GPIO_NONE, // GPIO03 + GPIO_KEY1, // GPIO04 + GPIO_NONE, // GPIO05 + // GPIO06 (SD_CLK Flash) + // GPIO07 (SD_DATA0 Flash QIO/DIO/DOUT) + // GPIO08 (SD_DATA1 Flash QIO/DIO/DOUT) + GPIO_KEY2, // GPIO09 (SD_DATA2 Flash QIO or ESP8285) + GPIO_NONE, // GPIO10 (SD_DATA3 Flash QIO or ESP8285) + // GPIO11 (SD_CMD Flash) + GPIO_REL2, // GPIO12 + GPIO_RFRECV, // GPIO13 + GPIO_REL1, // GPIO14 + GPIO_LED3, // GPIO15 + GPIO_LED_POWER_INV // GPIO16 Led (1 = On, 0 = Off) - Link and Power status + }, + { + "3 Channel", // 3 Channel (ESP8285) + GPIO_LED1, // GPIO00 + GPIO_NONE, // GPIO01 + GPIO_LED2, // GPIO02 + GPIO_NONE, // GPIO03 + GPIO_KEY1, // GPIO04 + GPIO_REL3, // GPIO05 + // GPIO06 (SD_CLK Flash) + // GPIO07 (SD_DATA0 Flash QIO/DIO/DOUT) + // GPIO08 (SD_DATA1 Flash QIO/DIO/DOUT) + GPIO_KEY2, // GPIO09 (SD_DATA2 Flash QIO or ESP8285) + GPIO_KEY3, // GPIO10 (SD_DATA3 Flash QIO or ESP8285) + // GPIO11 (SD_CMD Flash) + GPIO_REL2, // GPIO12 + GPIO_RFRECV, // GPIO13 + GPIO_REL1, // GPIO14 + GPIO_LED3, // GPIO15 + GPIO_LED_POWER_INV // GPIO16 Led (1 = On, 0 = Off) - Link and Power status + }, + { + "iciness CH3", // 3 Channel (ESP8285) + GPIO_NONE, // GPIO00 + GPIO_NONE, // GPIO01 + GPIO_KEY3, // GPIO02 + GPIO_NONE, // GPIO03 + GPIO_LED1, // GPIO04 + GPIO_LED2, // GPIO05 + // GPIO06 (SD_CLK Flash) + // GPIO07 (SD_DATA0 Flash QIO/DIO/DOUT) + // GPIO08 (SD_DATA1 Flash QIO/DIO/DOUT) + GPIO_KEY1, // GPIO09 (SD_DATA2 Flash QIO or ESP8285) + GPIO_KEY2, // GPIO10 (SD_DATA3 Flash QIO or ESP8285) + // GPIO11 (SD_CMD Flash) + GPIO_REL3, // GPIO12 + GPIO_NONE, // GPIO13 + GPIO_REL1, // GPIO14 + GPIO_REL2, // GPIO15 + GPIO_LED3 // GPIO16 Led (1 = On, 0 = Off) - Link and Power status + }, +}; + +#endif \ No newline at end of file diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/README @@ -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 a 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 +#include + +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 diff --git a/lib/esp_framework/include/Config.h b/lib/esp_framework/include/Config.h new file mode 100644 index 0000000..d7492d6 --- /dev/null +++ b/lib/esp_framework/include/Config.h @@ -0,0 +1,60 @@ +// Config.h + +#ifndef _CONFIG_h +#define _CONFIG_h + +#include +#include +#include +#include +#include "Arduino.h" +#include "GlobalConfig.pb.h" + +#if PB_PROTO_HEADER_VERSION != 30 +#error Regenerate this file with the current version of nanopb generator. +#endif + +#define GLOBAL_CFG_VERSION 1 // 1 - 999 + +//#define WIFI_SSID "qlwz" // WiFi ssid +//#define WIFI_PASS "" // WiFi 密码 + +//#define MQTT_SERVER "10.0.0.25" // MQTTַ 地址 +//#define MQTT_PORT 1883 // MQTT 端口 +//#define MQTT_USER "mqtt" // MQTT 用户名 +//#define MQTT_PASS "" // MQTT 密码 + +#define MQTT_FULLTOPIC "%module%/%hostname%/%prefix%/" // MQTT 主题格式 + +#define OTA_URL "http://10.0.0.50/esp/%module%.bin" + +#define WEB_LOG_SIZE 4000 // Max number of characters in weblog +#define BOOT_LOOP_OFFSET 5 // 开始恢复默认值之前的引导循环数 (0 = disable, 1..200 = 循环次数) + +extern char UID[16]; +extern char tmpData[512]; +extern GlobalConfigMessage globalConfig; +extern uint32_t perSecond; +extern Ticker *tickerPerSecond; + +class Config +{ +private: + static uint16_t nowCrc; + static bool isDelay; + static uint8_t countdown; + +public: + static uint16_t crc16(uint8_t *ptr, uint16_t len); + + static void readConfig(); + static void resetConfig(); + static bool saveConfig(bool isEverySecond = false); + static void delaySaveConfig(uint8_t second); + + static void moduleReadConfig(uint16_t version, uint16_t size, const pb_field_t fields[], void *dest_struct); + static bool moduleSaveConfig(uint16_t version, uint16_t size, const pb_field_t fields[], const void *src_struct); + + static void perSecondDo(); +}; +#endif diff --git a/lib/esp_framework/include/Debug.h b/lib/esp_framework/include/Debug.h new file mode 100644 index 0000000..6a54187 --- /dev/null +++ b/lib/esp_framework/include/Debug.h @@ -0,0 +1,39 @@ +// Debug.h + +#ifndef _DEBUG_h +#define _DEBUG_h + +#include +#include "Config.h" + +enum LoggingLevels +{ + LOG_LEVEL_NONE, + LOG_LEVEL_ERROR, + LOG_LEVEL_INFO, + LOG_LEVEL_DEBUG, + LOG_LEVEL_DEBUG_MORE, + LOG_LEVEL_ALL +}; + +class Debug +{ +protected: + static size_t strchrspn(const char *str1, int character); + +public: + static uint8_t webLogIndex; + static char webLog[WEB_LOG_SIZE]; + static void GetLog(uint8_t idx, char **entry_pp, uint16_t *len_p); + + static IPAddress ip; + static void Syslog(); + static void AddLog(uint8_t loglevel); + static void AddLog(uint8_t loglevel, PGM_P formatP, ...); + + static void AddInfo(PGM_P formatP, ...); + static void AddDebug(PGM_P formatP, ...); + static void AddError(PGM_P formatP, ...); +}; + +#endif diff --git a/lib/esp_framework/include/Framework.h b/lib/esp_framework/include/Framework.h new file mode 100644 index 0000000..1b72967 --- /dev/null +++ b/lib/esp_framework/include/Framework.h @@ -0,0 +1,22 @@ + +// Framework.h + +#ifndef _FRAMEWORK_h +#define _FRAMEWORK_h + +#include "Arduino.h" + +class Framework +{ + static uint16_t rebootCount; + static void callback(char *topic, byte *payload, unsigned int length); + static void connectedCallback(); + static void tickerPerSecondDo(); + +public: + static void one(unsigned long baud); + static void setup(); + static void loop(); +}; + +#endif \ No newline at end of file diff --git a/lib/esp_framework/include/GlobalConfig.pb.h b/lib/esp_framework/include/GlobalConfig.pb.h new file mode 100644 index 0000000..70a2524 --- /dev/null +++ b/lib/esp_framework/include/GlobalConfig.pb.h @@ -0,0 +1,94 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.3.9.4 at Sat Feb 15 11:23:30 2020. */ + +#ifndef PB_GLOBALCONFIG_PB_H_INCLUDED +#define PB_GLOBALCONFIG_PB_H_INCLUDED +#include + +/* @@protoc_insertion_point(includes) */ +#if PB_PROTO_HEADER_VERSION != 30 +#error Regenerate this file with the current version of nanopb generator. +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* Struct definitions */ +typedef struct _DebugConfigMessage { + uint8_t type; + char server[40]; + uint16_t port; +/* @@protoc_insertion_point(struct:DebugConfigMessage) */ +} DebugConfigMessage; + +typedef struct _HttpConfigMessage { + uint16_t port; + char user[15]; + char pass[15]; + char ota_url[150]; +/* @@protoc_insertion_point(struct:HttpConfigMessage) */ +} HttpConfigMessage; + +typedef struct _MqttConfigMessage { + char server[30]; + uint16_t port; + char user[20]; + char pass[30]; + bool retain; + char topic[50]; + bool discovery; + char discovery_prefix[30]; + uint16_t interval; +/* @@protoc_insertion_point(struct:MqttConfigMessage) */ +} MqttConfigMessage; + +typedef struct _WifiConfigMessage { + char ssid[20]; + char pass[30]; + bool is_static; + char ip[15]; + char sn[15]; + char gw[15]; + char ntp[40]; +/* @@protoc_insertion_point(struct:WifiConfigMessage) */ +} WifiConfigMessage; + +typedef PB_BYTES_ARRAY_T(500) GlobalConfigMessage_module_cfg_t; +typedef struct _GlobalConfigMessage { + WifiConfigMessage wifi; + HttpConfigMessage http; + MqttConfigMessage mqtt; + DebugConfigMessage debug; + uint16_t cfg_version; + uint16_t module_crc; + GlobalConfigMessage_module_cfg_t module_cfg; + char uid[20]; +/* @@protoc_insertion_point(struct:GlobalConfigMessage) */ +} GlobalConfigMessage; + + +/* Struct field encoding specification for nanopb */ +extern const pb_field_t GlobalConfigMessage_fields[9]; +extern const pb_field_t WifiConfigMessage_fields[8]; +extern const pb_field_t HttpConfigMessage_fields[5]; +extern const pb_field_t MqttConfigMessage_fields[10]; +extern const pb_field_t DebugConfigMessage_fields[4]; + +/* Maximum encoded size of messages (where known) */ +#define GlobalConfigMessage_size 1130 + +/* Message IDs (where set with "msgid" option) */ +#ifdef PB_MSGID + +#define GLOBALCONFIG_MESSAGES \ + + +#endif + +#ifdef __cplusplus +} /* extern "C" */ +#endif +/* @@protoc_insertion_point(eof) */ + +#endif diff --git a/lib/esp_framework/include/Http.h b/lib/esp_framework/include/Http.h new file mode 100644 index 0000000..c5a54b7 --- /dev/null +++ b/lib/esp_framework/include/Http.h @@ -0,0 +1,40 @@ +// Http.h + +#ifndef _HTTP_h +#define _HTTP_h + +#include "Arduino.h" +#include +#include + +class Http +{ +private: + static bool isBegin; + static void handleRoot(); + static void handleMqtt(); + static void handledhcp(); + static void handleScanWifi(); + static void handleWifi(); + static void handleDiscovery(); + static void handleRestart(); + static void handleReset(); + static void handleNotFound(); + static void handleModuleSetting(); + static void handleOTA(); + static void handleGetStatus(); + static void handleUpdate(); + static bool checkAuth(); + static String updaterError; + +public: + static ESP8266WebServer *server; + static void begin(); + static void stop(); + static void loop(); + static bool captivePortal(); + + static void OTA(String url); +}; + +#endif diff --git a/lib/esp_framework/include/Led.h b/lib/esp_framework/include/Led.h new file mode 100644 index 0000000..1e19158 --- /dev/null +++ b/lib/esp_framework/include/Led.h @@ -0,0 +1,30 @@ +// Led.h + +#ifndef _LED_h +#define _LED_h + +#include "Arduino.h" +#include + +class Led +{ +protected: + static uint8_t io; + static uint8_t light; + static Ticker *ledTicker; + static Ticker *ledTicker2; + static uint8_t ledType; + static bool isOn; + +public: + static void init(uint8_t _io, uint8_t _light); + static void loop(); + static void led(int ms = 200); + static void blinkLED(int duration, int n); + + static void on(); + static void off(); + static void toggle(); +}; + +#endif diff --git a/lib/esp_framework/include/Module.h b/lib/esp_framework/include/Module.h new file mode 100644 index 0000000..9a7492d --- /dev/null +++ b/lib/esp_framework/include/Module.h @@ -0,0 +1,42 @@ +// Module.h + +#ifndef _MODULE_h +#define _MODULE_h + +#include +#include "Config.h" +#include "Led.h" +#include "Wifi.h" +#include "Mqtt.h" +#include "Debug.h" +#include "Util.h" + +class Module +{ +public: + virtual void init(); + virtual String getModuleName(); + virtual String getModuleCNName(); + virtual String getModuleVersion(); + virtual String getModuleAuthor(); + + virtual bool moduleLed(); + + virtual void loop(); + virtual void perSecondDo(); + + virtual void readConfig(); + virtual void resetConfig(); + virtual void saveConfig(bool isEverySecond); + + virtual void httpAdd(ESP8266WebServer *server); + virtual void httpHtml(ESP8266WebServer *server); + virtual String httpGetStatus(ESP8266WebServer *server); + + virtual void mqttCallback(String topicStr, String str); + virtual void mqttConnected(); + virtual void mqttDiscovery(bool isEnable = true); +}; + +extern Module *module; +#endif diff --git a/lib/esp_framework/include/Mqtt.h b/lib/esp_framework/include/Mqtt.h new file mode 100644 index 0000000..be0c606 --- /dev/null +++ b/lib/esp_framework/include/Mqtt.h @@ -0,0 +1,59 @@ +// Mqtt.h + +#ifndef _MQTT_h +#define _MQTT_h + +#define MQTT_SOCKET_TIMEOUT 5 +#define MQTT_MAX_PACKET_SIZE 768 + +#include +#include "Arduino.h" + +#define MQTT_CONNECTED_CALLBACK_SIGNATURE std::function connectedcallback + +class Mqtt +{ +protected: + static String getTopic(uint8_t prefix, String subtopic); + static String topicCmnd; + static String topicStat; + static String topicTele; + static uint8_t operationFlag; + static void doReportHeartbeat(); + +public: + static PubSubClient mqttClient; + static MQTT_CONNECTED_CALLBACK_SIGNATURE; + + static uint32_t lastReconnectAttempt; // 最后尝试重连时间 + static uint32_t kMqttReconnectTime; // 重新连接尝试之间的延迟(ms) + + static bool mqttConnect(); + static void availability(); + static void loop(); + static void mqttSetLoopCallback(MQTT_CALLBACK_SIGNATURE); + static void mqttSetConnectedCallback(MQTT_CONNECTED_CALLBACK_SIGNATURE); + + static void setTopic(); + static String getCmndTopic(String topic); + static String getStatTopic(String topic); + static String getTeleTopic(String topic); + + static PubSubClient &setClient(Client &client); + static bool publish(String topic, const char *payload); + static bool publish(String topic, const char *payload, bool retained); + + static bool publish(const char *topic, const char *payload); + static bool publish(const char *topic, const char *payload, bool retained); + static bool publish(const char *topic, const uint8_t *payload, unsigned int plength); + static bool publish(const char *topic, const uint8_t *payload, unsigned int plength, bool retained); + static bool publish_P(const char *topic, const char *payload, bool retained); + static bool publish_P(const char *topic, const uint8_t *payload, unsigned int plength, bool retained); + + static bool subscribe(String topic); + static bool subscribe(String topic, uint8_t qos); + static bool unsubscribe(String topic); + static void perSecondDo(); +}; + +#endif diff --git a/lib/esp_framework/include/Rtc.h b/lib/esp_framework/include/Rtc.h new file mode 100644 index 0000000..965e42a --- /dev/null +++ b/lib/esp_framework/include/Rtc.h @@ -0,0 +1,59 @@ +// Ntp.h + +#ifndef _NTP_h +#define _NTP_h + +#include "Arduino.h" + +#define LEAP_YEAR(Y) (((1970 + Y) > 0) && !((1970 + Y) % 4) && (((1970 + Y) % 100) || !((1970 + Y) % 400))) + +typedef struct +{ + uint8_t second; + uint8_t minute; + uint8_t hour; + uint8_t day_of_week; // sunday is day 1 + uint8_t day_of_month; + uint8_t month; + uint16_t day_of_year; + uint16_t year; + unsigned long days; + bool valid; +} TIME_T; + +typedef struct _RtcReboot +{ + uint16_t valid; // 280 (RTC memory offset 100 - sizeof(RTCRBT)) + uint8_t fast_reboot_count; // 282 + uint8_t free_003[1]; // 283 +} RtcReboot; + +const uint16_t RTC_MEM_VALID = 0xA55A; + +class Rtc +{ +protected: + static uint32_t rtcRebootCrc; + static uint8_t operationFlag; + static void getNtp(); + +public: + static void breakTime(uint32_t time_input, TIME_T &tm); + static uint32_t utcTime; + static RtcReboot rtcReboot; + static TIME_T rtcTime; + + static String msToHumanString(uint32_t const msecs); + static String timeSince(uint32_t const start); + + static String GetBuildDateAndTime(); + static void perSecondDo(); + static void init(); + static void loop(); + + static uint32_t getRtcRebootCrc(); + static void rtcRebootLoad(); + static void rtcRebootSave(); +}; + +#endif diff --git a/lib/esp_framework/include/Util.h b/lib/esp_framework/include/Util.h new file mode 100644 index 0000000..0f7197e --- /dev/null +++ b/lib/esp_framework/include/Util.h @@ -0,0 +1,16 @@ +#ifndef _Util_h +#define _Util_h + +#include "Arduino.h" + +class Util +{ +public: + static char *strlowr(char *str); + static char *strupr(char *str); + static uint16_t hex2Str(uint8_t *bin, uint16_t bin_size, char *buff, bool needBlank = true); + static char *dtostrfd(double number, unsigned char prec, char *s); + static uint32_t SqrtInt(uint32_t num); + static uint32_t RoundSqrtInt(uint32_t num); +}; +#endif \ No newline at end of file diff --git a/lib/esp_framework/include/Wifi.h b/lib/esp_framework/include/Wifi.h new file mode 100644 index 0000000..16776f4 --- /dev/null +++ b/lib/esp_framework/include/Wifi.h @@ -0,0 +1,42 @@ +// Wifi.h + +#ifndef _WIFI_h +#define _WIFI_h + +#include +#include +#include "Arduino.h" + +//#define ConnectTimeOut 300 +#define ConfigPortalTimeOut 120 +#define MinimumWifiSignalQuality 8 + +class Wifi +{ +private: + static bool connect; + + static String _ssid; + static String _pass; + + static DNSServer *dnsServer; + //static unsigned long connectStart; + +public: + static unsigned long configPortalStart; + static bool isDHCP; + static WiFiEventHandler STAGotIP; + //static WiFiEventHandler STADisconnected; + static WiFiClient wifiClient; + static void connectWifi(); + static void setupWifi(); + static void setupWifiManager(bool resetSettings); + static bool isIp(String str); + + static uint8_t waitForConnectResult(); + static void tryConnect(String ssid, String pass); + + static void loop(); +}; + +#endif diff --git a/lib/esp_framework/library.json b/lib/esp_framework/library.json new file mode 100644 index 0000000..25f56df --- /dev/null +++ b/lib/esp_framework/library.json @@ -0,0 +1,8 @@ +{ + "name": "esp_framework", + "keywords": "esp framework", + "description": "esp framework", + "version": "1.0", + "frameworks": "arduino", + "platforms": "espressif8266" +} \ No newline at end of file diff --git a/lib/esp_framework/nanopb/GlobalConfig.proto b/lib/esp_framework/nanopb/GlobalConfig.proto new file mode 100644 index 0000000..c8563de --- /dev/null +++ b/lib/esp_framework/nanopb/GlobalConfig.proto @@ -0,0 +1,52 @@ + +syntax = "proto3"; + +import "nanopb.proto"; + +message GlobalConfigMessage { + WifiConfigMessage wifi = 1; + HttpConfigMessage http = 2; + MqttConfigMessage mqtt = 3; + DebugConfigMessage debug = 4; + uint32 cfg_version = 5 [(nanopb).int_size = IS_16]; + uint32 module_crc = 6 [(nanopb).int_size = IS_16]; + bytes module_cfg = 7 [(nanopb).max_size = 500]; + string uid = 8 [(nanopb).max_size = 20]; +} + +message WifiConfigMessage { + string ssid = 1 [(nanopb).max_size = 20]; + string pass = 2 [(nanopb).max_size = 30]; + + bool is_static = 3; + string ip = 4 [(nanopb).max_size = 15]; + string sn = 5 [(nanopb).max_size = 15]; + string gw = 6 [(nanopb).max_size = 15]; + + string ntp = 7 [(nanopb).max_size = 40]; +} + +message HttpConfigMessage { + uint32 port = 1 [(nanopb).int_size = IS_16]; + string user = 2 [(nanopb).max_size = 15]; + string pass = 3 [(nanopb).max_size = 15]; + string ota_url = 4 [(nanopb).max_size = 150]; +} + +message MqttConfigMessage { + string server = 1 [(nanopb).max_size = 30]; + uint32 port = 2 [(nanopb).int_size = IS_16]; + string user = 3 [(nanopb).max_size = 20]; + string pass = 4 [(nanopb).max_size = 30]; + bool retain = 5; + string topic = 6 [(nanopb).max_size = 50]; + bool discovery = 7; + string discovery_prefix = 8 [(nanopb).max_size = 30]; + uint32 interval = 9 [(nanopb).int_size = IS_16]; +} + +message DebugConfigMessage { + uint32 type = 1 [(nanopb).int_size = IS_8]; + string server = 2 [(nanopb).max_size = 40]; + uint32 port = 3 [(nanopb).int_size = IS_16]; +} diff --git a/lib/esp_framework/nanopb/descriptor.proto b/lib/esp_framework/nanopb/descriptor.proto new file mode 100644 index 0000000..8697a50 --- /dev/null +++ b/lib/esp_framework/nanopb/descriptor.proto @@ -0,0 +1,872 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: kenton@google.com (Kenton Varda) +// Based on original Protocol Buffers design by +// Sanjay Ghemawat, Jeff Dean, and others. +// +// The messages in this file describe the definitions found in .proto files. +// A valid .proto file can be translated directly to a FileDescriptorProto +// without any other information (e.g. without reading its imports). + + +syntax = "proto2"; + +package google.protobuf; +option go_package = "github.com/golang/protobuf/protoc-gen-go/descriptor;descriptor"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "DescriptorProtos"; +option csharp_namespace = "Google.Protobuf.Reflection"; +option objc_class_prefix = "GPB"; +option cc_enable_arenas = true; + +// descriptor.proto must be optimized for speed because reflection-based +// algorithms don't work during bootstrapping. +option optimize_for = SPEED; + +// The protocol compiler can output a FileDescriptorSet containing the .proto +// files it parses. +message FileDescriptorSet { + repeated FileDescriptorProto file = 1; +} + +// Describes a complete .proto file. +message FileDescriptorProto { + optional string name = 1; // file name, relative to root of source tree + optional string package = 2; // e.g. "foo", "foo.bar", etc. + + // Names of files imported by this file. + repeated string dependency = 3; + // Indexes of the public imported files in the dependency list above. + repeated int32 public_dependency = 10; + // Indexes of the weak imported files in the dependency list. + // For Google-internal migration only. Do not use. + repeated int32 weak_dependency = 11; + + // All top-level definitions in this file. + repeated DescriptorProto message_type = 4; + repeated EnumDescriptorProto enum_type = 5; + repeated ServiceDescriptorProto service = 6; + repeated FieldDescriptorProto extension = 7; + + optional FileOptions options = 8; + + // This field contains optional information about the original source code. + // You may safely remove this entire field without harming runtime + // functionality of the descriptors -- the information is needed only by + // development tools. + optional SourceCodeInfo source_code_info = 9; + + // The syntax of the proto file. + // The supported values are "proto2" and "proto3". + optional string syntax = 12; +} + +// Describes a message type. +message DescriptorProto { + optional string name = 1; + + repeated FieldDescriptorProto field = 2; + repeated FieldDescriptorProto extension = 6; + + repeated DescriptorProto nested_type = 3; + repeated EnumDescriptorProto enum_type = 4; + + message ExtensionRange { + optional int32 start = 1; + optional int32 end = 2; + + optional ExtensionRangeOptions options = 3; + } + repeated ExtensionRange extension_range = 5; + + repeated OneofDescriptorProto oneof_decl = 8; + + optional MessageOptions options = 7; + + // Range of reserved tag numbers. Reserved tag numbers may not be used by + // fields or extension ranges in the same message. Reserved ranges may + // not overlap. + message ReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Exclusive. + } + repeated ReservedRange reserved_range = 9; + // Reserved field names, which may not be used by fields in the same message. + // A given name may only be reserved once. + repeated string reserved_name = 10; +} + +message ExtensionRangeOptions { + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +// Describes a field within a message. +message FieldDescriptorProto { + enum Type { + // 0 is reserved for errors. + // Order is weird for historical reasons. + TYPE_DOUBLE = 1; + TYPE_FLOAT = 2; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if + // negative values are likely. + TYPE_INT64 = 3; + TYPE_UINT64 = 4; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if + // negative values are likely. + TYPE_INT32 = 5; + TYPE_FIXED64 = 6; + TYPE_FIXED32 = 7; + TYPE_BOOL = 8; + TYPE_STRING = 9; + // Tag-delimited aggregate. + // Group type is deprecated and not supported in proto3. However, Proto3 + // implementations should still be able to parse the group wire format and + // treat group fields as unknown fields. + TYPE_GROUP = 10; + TYPE_MESSAGE = 11; // Length-delimited aggregate. + + // New in version 2. + TYPE_BYTES = 12; + TYPE_UINT32 = 13; + TYPE_ENUM = 14; + TYPE_SFIXED32 = 15; + TYPE_SFIXED64 = 16; + TYPE_SINT32 = 17; // Uses ZigZag encoding. + TYPE_SINT64 = 18; // Uses ZigZag encoding. + }; + + enum Label { + // 0 is reserved for errors + LABEL_OPTIONAL = 1; + LABEL_REQUIRED = 2; + LABEL_REPEATED = 3; + }; + + optional string name = 1; + optional int32 number = 3; + optional Label label = 4; + + // If type_name is set, this need not be set. If both this and type_name + // are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. + optional Type type = 5; + + // For message and enum types, this is the name of the type. If the name + // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping + // rules are used to find the type (i.e. first the nested types within this + // message are searched, then within the parent, on up to the root + // namespace). + optional string type_name = 6; + + // For extensions, this is the name of the type being extended. It is + // resolved in the same manner as type_name. + optional string extendee = 2; + + // For numeric types, contains the original text representation of the value. + // For booleans, "true" or "false". + // For strings, contains the default text contents (not escaped in any way). + // For bytes, contains the C escaped value. All bytes >= 128 are escaped. + // TODO(kenton): Base-64 encode? + optional string default_value = 7; + + // If set, gives the index of a oneof in the containing type's oneof_decl + // list. This field is a member of that oneof. + optional int32 oneof_index = 9; + + // JSON name of this field. The value is set by protocol compiler. If the + // user has set a "json_name" option on this field, that option's value + // will be used. Otherwise, it's deduced from the field's name by converting + // it to camelCase. + optional string json_name = 10; + + optional FieldOptions options = 8; +} + +// Describes a oneof. +message OneofDescriptorProto { + optional string name = 1; + optional OneofOptions options = 2; +} + +// Describes an enum type. +message EnumDescriptorProto { + optional string name = 1; + + repeated EnumValueDescriptorProto value = 2; + + optional EnumOptions options = 3; + + // Range of reserved numeric values. Reserved values may not be used by + // entries in the same enum. Reserved ranges may not overlap. + // + // Note that this is distinct from DescriptorProto.ReservedRange in that it + // is inclusive such that it can appropriately represent the entire int32 + // domain. + message EnumReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Inclusive. + } + + // Range of reserved numeric values. Reserved numeric values may not be used + // by enum values in the same enum declaration. Reserved ranges may not + // overlap. + repeated EnumReservedRange reserved_range = 4; + + // Reserved enum value names, which may not be reused. A given name may only + // be reserved once. + repeated string reserved_name = 5; +} + +// Describes a value within an enum. +message EnumValueDescriptorProto { + optional string name = 1; + optional int32 number = 2; + + optional EnumValueOptions options = 3; +} + +// Describes a service. +message ServiceDescriptorProto { + optional string name = 1; + repeated MethodDescriptorProto method = 2; + + optional ServiceOptions options = 3; +} + +// Describes a method of a service. +message MethodDescriptorProto { + optional string name = 1; + + // Input and output type names. These are resolved in the same way as + // FieldDescriptorProto.type_name, but must refer to a message type. + optional string input_type = 2; + optional string output_type = 3; + + optional MethodOptions options = 4; + + // Identifies if client streams multiple client messages + optional bool client_streaming = 5 [default=false]; + // Identifies if server streams multiple server messages + optional bool server_streaming = 6 [default=false]; +} + + +// =================================================================== +// Options + +// Each of the definitions above may have "options" attached. These are +// just annotations which may cause code to be generated slightly differently +// or may contain hints for code that manipulates protocol messages. +// +// Clients may define custom options as extensions of the *Options messages. +// These extensions may not yet be known at parsing time, so the parser cannot +// store the values in them. Instead it stores them in a field in the *Options +// message called uninterpreted_option. This field must have the same name +// across all *Options messages. We then use this field to populate the +// extensions when we build a descriptor, at which point all protos have been +// parsed and so all extensions are known. +// +// Extension numbers for custom options may be chosen as follows: +// * For options which will only be used within a single application or +// organization, or for experimental options, use field numbers 50000 +// through 99999. It is up to you to ensure that you do not use the +// same number for multiple options. +// * For options which will be published and used publicly by multiple +// independent entities, e-mail protobuf-global-extension-registry@google.com +// to reserve extension numbers. Simply provide your project name (e.g. +// Objective-C plugin) and your project website (if available) -- there's no +// need to explain how you intend to use them. Usually you only need one +// extension number. You can declare multiple options with only one extension +// number by putting them in a sub-message. See the Custom Options section of +// the docs for examples: +// https://developers.google.com/protocol-buffers/docs/proto#options +// If this turns out to be popular, a web service will be set up +// to automatically assign option numbers. + + +message FileOptions { + + // Sets the Java package where classes generated from this .proto will be + // placed. By default, the proto package is used, but this is often + // inappropriate because proto packages do not normally start with backwards + // domain names. + optional string java_package = 1; + + + // If set, all the classes from the .proto file are wrapped in a single + // outer class with the given name. This applies to both Proto1 + // (equivalent to the old "--one_java_file" option) and Proto2 (where + // a .proto always translates to a single class, but you may want to + // explicitly choose the class name). + optional string java_outer_classname = 8; + + // If set true, then the Java code generator will generate a separate .java + // file for each top-level message, enum, and service defined in the .proto + // file. Thus, these types will *not* be nested inside the outer class + // named by java_outer_classname. However, the outer class will still be + // generated to contain the file's getDescriptor() method as well as any + // top-level extensions defined in the file. + optional bool java_multiple_files = 10 [default=false]; + + // This option does nothing. + optional bool java_generate_equals_and_hash = 20 [deprecated=true]; + + // If set true, then the Java2 code generator will generate code that + // throws an exception whenever an attempt is made to assign a non-UTF-8 + // byte sequence to a string field. + // Message reflection will do the same. + // However, an extension field still accepts non-UTF-8 byte sequences. + // This option has no effect on when used with the lite runtime. + optional bool java_string_check_utf8 = 27 [default=false]; + + + // Generated classes can be optimized for speed or code size. + enum OptimizeMode { + SPEED = 1; // Generate complete code for parsing, serialization, + // etc. + CODE_SIZE = 2; // Use ReflectionOps to implement these methods. + LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime. + } + optional OptimizeMode optimize_for = 9 [default=SPEED]; + + // Sets the Go package where structs generated from this .proto will be + // placed. If omitted, the Go package will be derived from the following: + // - The basename of the package import path, if provided. + // - Otherwise, the package statement in the .proto file, if present. + // - Otherwise, the basename of the .proto file, without extension. + optional string go_package = 11; + + + + // Should generic services be generated in each language? "Generic" services + // are not specific to any particular RPC system. They are generated by the + // main code generators in each language (without additional plugins). + // Generic services were the only kind of service generation supported by + // early versions of google.protobuf. + // + // Generic services are now considered deprecated in favor of using plugins + // that generate code specific to your particular RPC system. Therefore, + // these default to false. Old code which depends on generic services should + // explicitly set them to true. + optional bool cc_generic_services = 16 [default=false]; + optional bool java_generic_services = 17 [default=false]; + optional bool py_generic_services = 18 [default=false]; + optional bool php_generic_services = 42 [default=false]; + + // Is this file deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for everything in the file, or it will be completely ignored; in the very + // least, this is a formalization for deprecating files. + optional bool deprecated = 23 [default=false]; + + // Enables the use of arenas for the proto messages in this file. This applies + // only to generated classes for C++. + optional bool cc_enable_arenas = 31 [default=false]; + + + // Sets the objective c class prefix which is prepended to all objective c + // generated classes from this .proto. There is no default. + optional string objc_class_prefix = 36; + + // Namespace for generated classes; defaults to the package. + optional string csharp_namespace = 37; + + // By default Swift generators will take the proto package and CamelCase it + // replacing '.' with underscore and use that to prefix the types/symbols + // defined. When this options is provided, they will use this value instead + // to prefix the types/symbols defined. + optional string swift_prefix = 39; + + // Sets the php class prefix which is prepended to all php generated classes + // from this .proto. Default is empty. + optional string php_class_prefix = 40; + + // Use this option to change the namespace of php generated classes. Default + // is empty. When this option is empty, the package name will be used for + // determining the namespace. + optional string php_namespace = 41; + + // The parser stores options it doesn't recognize here. + // See the documentation for the "Options" section above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. + // See the documentation for the "Options" section above. + extensions 1000 to max; + + reserved 38; +} + +message MessageOptions { + // Set true to use the old proto1 MessageSet wire format for extensions. + // This is provided for backwards-compatibility with the MessageSet wire + // format. You should not use this for any other reason: It's less + // efficient, has fewer features, and is more complicated. + // + // The message must be defined exactly as follows: + // message Foo { + // option message_set_wire_format = true; + // extensions 4 to max; + // } + // Note that the message cannot have any defined fields; MessageSets only + // have extensions. + // + // All extensions of your type must be singular messages; e.g. they cannot + // be int32s, enums, or repeated messages. + // + // Because this is an option, the above two restrictions are not enforced by + // the protocol compiler. + optional bool message_set_wire_format = 1 [default=false]; + + // Disables the generation of the standard "descriptor()" accessor, which can + // conflict with a field of the same name. This is meant to make migration + // from proto1 easier; new code should avoid fields named "descriptor". + optional bool no_standard_descriptor_accessor = 2 [default=false]; + + // Is this message deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the message, or it will be completely ignored; in the very least, + // this is a formalization for deprecating messages. + optional bool deprecated = 3 [default=false]; + + // Whether the message is an automatically generated map entry type for the + // maps field. + // + // For maps fields: + // map map_field = 1; + // The parsed descriptor looks like: + // message MapFieldEntry { + // option map_entry = true; + // optional KeyType key = 1; + // optional ValueType value = 2; + // } + // repeated MapFieldEntry map_field = 1; + // + // Implementations may choose not to generate the map_entry=true message, but + // use a native map in the target language to hold the keys and values. + // The reflection APIs in such implementions still need to work as + // if the field is a repeated message field. + // + // NOTE: Do not set the option in .proto files. Always use the maps syntax + // instead. The option should only be implicitly set by the proto compiler + // parser. + optional bool map_entry = 7; + + reserved 8; // javalite_serializable + reserved 9; // javanano_as_lite + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message FieldOptions { + // The ctype option instructs the C++ code generator to use a different + // representation of the field than it normally would. See the specific + // options below. This option is not yet implemented in the open source + // release -- sorry, we'll try to include it in a future version! + optional CType ctype = 1 [default = STRING]; + enum CType { + // Default mode. + STRING = 0; + + CORD = 1; + + STRING_PIECE = 2; + } + // The packed option can be enabled for repeated primitive fields to enable + // a more efficient representation on the wire. Rather than repeatedly + // writing the tag and type for each element, the entire array is encoded as + // a single length-delimited blob. In proto3, only explicit setting it to + // false will avoid using packed encoding. + optional bool packed = 2; + + // The jstype option determines the JavaScript type used for values of the + // field. The option is permitted only for 64 bit integral and fixed types + // (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING + // is represented as JavaScript string, which avoids loss of precision that + // can happen when a large value is converted to a floating point JavaScript. + // Specifying JS_NUMBER for the jstype causes the generated JavaScript code to + // use the JavaScript "number" type. The behavior of the default option + // JS_NORMAL is implementation dependent. + // + // This option is an enum to permit additional types to be added, e.g. + // goog.math.Integer. + optional JSType jstype = 6 [default = JS_NORMAL]; + enum JSType { + // Use the default type. + JS_NORMAL = 0; + + // Use JavaScript strings. + JS_STRING = 1; + + // Use JavaScript numbers. + JS_NUMBER = 2; + } + + // Should this field be parsed lazily? Lazy applies only to message-type + // fields. It means that when the outer message is initially parsed, the + // inner message's contents will not be parsed but instead stored in encoded + // form. The inner message will actually be parsed when it is first accessed. + // + // This is only a hint. Implementations are free to choose whether to use + // eager or lazy parsing regardless of the value of this option. However, + // setting this option true suggests that the protocol author believes that + // using lazy parsing on this field is worth the additional bookkeeping + // overhead typically needed to implement it. + // + // This option does not affect the public interface of any generated code; + // all method signatures remain the same. Furthermore, thread-safety of the + // interface is not affected by this option; const methods remain safe to + // call from multiple threads concurrently, while non-const methods continue + // to require exclusive access. + // + // + // Note that implementations may choose not to check required fields within + // a lazy sub-message. That is, calling IsInitialized() on the outer message + // may return true even if the inner message has missing required fields. + // This is necessary because otherwise the inner message would have to be + // parsed in order to perform the check, defeating the purpose of lazy + // parsing. An implementation which chooses not to check required fields + // must be consistent about it. That is, for any particular sub-message, the + // implementation must either *always* check its required fields, or *never* + // check its required fields, regardless of whether or not the message has + // been parsed. + optional bool lazy = 5 [default=false]; + + // Is this field deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for accessors, or it will be completely ignored; in the very least, this + // is a formalization for deprecating fields. + optional bool deprecated = 3 [default=false]; + + // For Google-internal migration only. Do not use. + optional bool weak = 10 [default=false]; + + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; + + reserved 4; // removed jtype +} + +message OneofOptions { + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumOptions { + + // Set this option to true to allow mapping different tag names to the same + // value. + optional bool allow_alias = 2; + + // Is this enum deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum, or it will be completely ignored; in the very least, this + // is a formalization for deprecating enums. + optional bool deprecated = 3 [default=false]; + + reserved 5; // javanano_as_lite + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumValueOptions { + // Is this enum value deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum value, or it will be completely ignored; in the very least, + // this is a formalization for deprecating enum values. + optional bool deprecated = 1 [default=false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message ServiceOptions { + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this service deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the service, or it will be completely ignored; in the very least, + // this is a formalization for deprecating services. + optional bool deprecated = 33 [default=false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message MethodOptions { + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this method deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the method, or it will be completely ignored; in the very least, + // this is a formalization for deprecating methods. + optional bool deprecated = 33 [default=false]; + + // Is this method side-effect-free (or safe in HTTP parlance), or idempotent, + // or neither? HTTP based RPC implementation may choose GET verb for safe + // methods, and PUT verb for idempotent methods instead of the default POST. + enum IdempotencyLevel { + IDEMPOTENCY_UNKNOWN = 0; + NO_SIDE_EFFECTS = 1; // implies idempotent + IDEMPOTENT = 2; // idempotent, but may have side effects + } + optional IdempotencyLevel idempotency_level = + 34 [default=IDEMPOTENCY_UNKNOWN]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + + +// A message representing a option the parser does not recognize. This only +// appears in options protos created by the compiler::Parser class. +// DescriptorPool resolves these when building Descriptor objects. Therefore, +// options protos in descriptor objects (e.g. returned by Descriptor::options(), +// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions +// in them. +message UninterpretedOption { + // The name of the uninterpreted option. Each string represents a segment in + // a dot-separated name. is_extension is true iff a segment represents an + // extension (denoted with parentheses in options specs in .proto files). + // E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents + // "foo.(bar.baz).qux". + message NamePart { + required string name_part = 1; + required bool is_extension = 2; + } + repeated NamePart name = 2; + + // The value of the uninterpreted option, in whatever type the tokenizer + // identified it as during parsing. Exactly one of these should be set. + optional string identifier_value = 3; + optional uint64 positive_int_value = 4; + optional int64 negative_int_value = 5; + optional double double_value = 6; + optional bytes string_value = 7; + optional string aggregate_value = 8; +} + +// =================================================================== +// Optional source code info + +// Encapsulates information about the original source file from which a +// FileDescriptorProto was generated. +message SourceCodeInfo { + // A Location identifies a piece of source code in a .proto file which + // corresponds to a particular definition. This information is intended + // to be useful to IDEs, code indexers, documentation generators, and similar + // tools. + // + // For example, say we have a file like: + // message Foo { + // optional string foo = 1; + // } + // Let's look at just the field definition: + // optional string foo = 1; + // ^ ^^ ^^ ^ ^^^ + // a bc de f ghi + // We have the following locations: + // span path represents + // [a,i) [ 4, 0, 2, 0 ] The whole field definition. + // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). + // [c,d) [ 4, 0, 2, 0, 5 ] The type (string). + // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). + // [g,h) [ 4, 0, 2, 0, 3 ] The number (1). + // + // Notes: + // - A location may refer to a repeated field itself (i.e. not to any + // particular index within it). This is used whenever a set of elements are + // logically enclosed in a single code segment. For example, an entire + // extend block (possibly containing multiple extension definitions) will + // have an outer location whose path refers to the "extensions" repeated + // field without an index. + // - Multiple locations may have the same path. This happens when a single + // logical declaration is spread out across multiple places. The most + // obvious example is the "extend" block again -- there may be multiple + // extend blocks in the same scope, each of which will have the same path. + // - A location's span is not always a subset of its parent's span. For + // example, the "extendee" of an extension declaration appears at the + // beginning of the "extend" block and is shared by all extensions within + // the block. + // - Just because a location's span is a subset of some other location's span + // does not mean that it is a descendent. For example, a "group" defines + // both a type and a field in a single declaration. Thus, the locations + // corresponding to the type and field and their components will overlap. + // - Code which tries to interpret locations should probably be designed to + // ignore those that it doesn't understand, as more types of locations could + // be recorded in the future. + repeated Location location = 1; + message Location { + // Identifies which part of the FileDescriptorProto was defined at this + // location. + // + // Each element is a field number or an index. They form a path from + // the root FileDescriptorProto to the place where the definition. For + // example, this path: + // [ 4, 3, 2, 7, 1 ] + // refers to: + // file.message_type(3) // 4, 3 + // .field(7) // 2, 7 + // .name() // 1 + // This is because FileDescriptorProto.message_type has field number 4: + // repeated DescriptorProto message_type = 4; + // and DescriptorProto.field has field number 2: + // repeated FieldDescriptorProto field = 2; + // and FieldDescriptorProto.name has field number 1: + // optional string name = 1; + // + // Thus, the above path gives the location of a field name. If we removed + // the last element: + // [ 4, 3, 2, 7 ] + // this path refers to the whole field declaration (from the beginning + // of the label to the terminating semicolon). + repeated int32 path = 1 [packed=true]; + + // Always has exactly three or four elements: start line, start column, + // end line (optional, otherwise assumed same as start line), end column. + // These are packed into a single field for efficiency. Note that line + // and column numbers are zero-based -- typically you will want to add + // 1 to each before displaying to a user. + repeated int32 span = 2 [packed=true]; + + // If this SourceCodeInfo represents a complete declaration, these are any + // comments appearing before and after the declaration which appear to be + // attached to the declaration. + // + // A series of line comments appearing on consecutive lines, with no other + // tokens appearing on those lines, will be treated as a single comment. + // + // leading_detached_comments will keep paragraphs of comments that appear + // before (but not connected to) the current element. Each paragraph, + // separated by empty lines, will be one comment element in the repeated + // field. + // + // Only the comment content is provided; comment markers (e.g. //) are + // stripped out. For block comments, leading whitespace and an asterisk + // will be stripped from the beginning of each line other than the first. + // Newlines are included in the output. + // + // Examples: + // + // optional int32 foo = 1; // Comment attached to foo. + // // Comment attached to bar. + // optional int32 bar = 2; + // + // optional string baz = 3; + // // Comment attached to baz. + // // Another line attached to baz. + // + // // Comment attached to qux. + // // + // // Another line attached to qux. + // optional double qux = 4; + // + // // Detached comment for corge. This is not leading or trailing comments + // // to qux or corge because there are blank lines separating it from + // // both. + // + // // Detached comment for corge paragraph 2. + // + // optional string corge = 5; + // /* Block comment attached + // * to corge. Leading asterisks + // * will be removed. */ + // /* Block comment attached to + // * grault. */ + // optional int32 grault = 6; + // + // // ignored detached comments. + optional string leading_comments = 3; + optional string trailing_comments = 4; + repeated string leading_detached_comments = 6; + } +} + +// Describes the relationship between generated code and its original source +// file. A GeneratedCodeInfo message is associated with only one generated +// source file, but may contain references to different source .proto files. +message GeneratedCodeInfo { + // An Annotation connects some span of text in generated code to an element + // of its generating .proto file. + repeated Annotation annotation = 1; + message Annotation { + // Identifies the element in the original source .proto file. This field + // is formatted the same as SourceCodeInfo.Location.path. + repeated int32 path = 1 [packed=true]; + + // Identifies the filesystem path to the original source .proto. + optional string source_file = 2; + + // Identifies the starting offset in bytes in the generated code + // that relates to the identified object. + optional int32 begin = 3; + + // Identifies the ending offset in bytes in the generated code that + // relates to the identified offset. The end offset should be one past + // the last relevant byte (so the length of the text = end - begin). + optional int32 end = 4; + } +} diff --git a/lib/esp_framework/nanopb/generator.bat b/lib/esp_framework/nanopb/generator.bat new file mode 100644 index 0000000..b768dd1 --- /dev/null +++ b/lib/esp_framework/nanopb/generator.bat @@ -0,0 +1,7 @@ +cd /d %~dp0 + +D:\Workspaces_Smarthome\esp\nanopb\generator-bin\protoc.exe --nanopb_out=. GlobalConfig.proto +copy GlobalConfig.pb.h ..\include /y +copy GlobalConfig.pb.c ..\src /y +del GlobalConfig.pb.h +del GlobalConfig.pb.c diff --git a/lib/esp_framework/nanopb/nanopb.proto b/lib/esp_framework/nanopb/nanopb.proto new file mode 100644 index 0000000..e89eea1 --- /dev/null +++ b/lib/esp_framework/nanopb/nanopb.proto @@ -0,0 +1,124 @@ +// Custom options for defining: +// - Maximum size of string/bytes +// - Maximum number of elements in array +// +// These are used by nanopb to generate statically allocable structures +// for memory-limited environments. + +syntax = "proto2"; +import "descriptor.proto"; + +option java_package = "fi.kapsi.koti.jpa.nanopb"; + +enum FieldType { + FT_DEFAULT = 0; // Automatically decide field type, generate static field if possible. + FT_CALLBACK = 1; // Always generate a callback field. + FT_POINTER = 4; // Always generate a dynamically allocated field. + FT_STATIC = 2; // Generate a static field or raise an exception if not possible. + FT_IGNORE = 3; // Ignore the field completely. + FT_INLINE = 5; // Legacy option, use the separate 'fixed_length' option instead +} + +enum IntSize { + IS_DEFAULT = 0; // Default, 32/64bit based on type in .proto + IS_8 = 8; + IS_16 = 16; + IS_32 = 32; + IS_64 = 64; +} + +enum TypenameMangling { + M_NONE = 0; // Default, no typename mangling + M_STRIP_PACKAGE = 1; // Strip current package name + M_FLATTEN = 2; // Only use last path component +} + +// This is the inner options message, which basically defines options for +// a field. When it is used in message or file scope, it applies to all +// fields. +message NanoPBOptions { + // Allocated size for 'bytes' and 'string' fields. + // For string fields, this should include the space for null terminator. + optional int32 max_size = 1; + + // Maximum length for 'string' fields. Setting this is equivalent + // to setting max_size to a value of length+1. + optional int32 max_length = 14; + + // Allocated number of entries in arrays ('repeated' fields) + optional int32 max_count = 2; + + // Size of integer fields. Can save some memory if you don't need + // full 32 bits for the value. + optional IntSize int_size = 7 [default = IS_DEFAULT]; + + // Force type of field (callback or static allocation) + optional FieldType type = 3 [default = FT_DEFAULT]; + + // Use long names for enums, i.e. EnumName_EnumValue. + optional bool long_names = 4 [default = true]; + + // Add 'packed' attribute to generated structs. + // Note: this cannot be used on CPUs that break on unaligned + // accesses to variables. + optional bool packed_struct = 5 [default = false]; + + // Add 'packed' attribute to generated enums. + optional bool packed_enum = 10 [default = false]; + + // Skip this message + optional bool skip_message = 6 [default = false]; + + // Generate oneof fields as normal optional fields instead of union. + optional bool no_unions = 8 [default = false]; + + // integer type tag for a message + optional uint32 msgid = 9; + + // decode oneof as anonymous union + optional bool anonymous_oneof = 11 [default = false]; + + // Proto3 singular field does not generate a "has_" flag + optional bool proto3 = 12 [default = false]; + + // Generate an enum->string mapping function (can take up lots of space). + optional bool enum_to_string = 13 [default = false]; + + // Generate bytes arrays with fixed length + optional bool fixed_length = 15 [default = false]; + + // Generate repeated field with fixed count + optional bool fixed_count = 16 [default = false]; + + // Mangle long names + optional TypenameMangling mangle_names = 17 [default = M_NONE]; +} + +// Extensions to protoc 'Descriptor' type in order to define options +// inside a .proto file. +// +// Protocol Buffers extension number registry +// -------------------------------- +// Project: Nanopb +// Contact: Petteri Aimonen +// Web site: http://kapsi.fi/~jpa/nanopb +// Extensions: 1010 (all types) +// -------------------------------- + +extend google.protobuf.FileOptions { + optional NanoPBOptions nanopb_fileopt = 1010; +} + +extend google.protobuf.MessageOptions { + optional NanoPBOptions nanopb_msgopt = 1010; +} + +extend google.protobuf.EnumOptions { + optional NanoPBOptions nanopb_enumopt = 1010; +} + +extend google.protobuf.FieldOptions { + optional NanoPBOptions nanopb = 1010; +} + + diff --git a/lib/esp_framework/src/Config.cpp b/lib/esp_framework/src/Config.cpp new file mode 100644 index 0000000..74ac220 --- /dev/null +++ b/lib/esp_framework/src/Config.cpp @@ -0,0 +1,243 @@ +#include +#include "Module.h" + +Module *module; +char UID[16]; +char tmpData[512] = {0}; +uint32_t perSecond; +Ticker *tickerPerSecond; +GlobalConfigMessage globalConfig; + +uint16_t Config::nowCrc; +uint8_t Config::countdown = 60; +bool Config::isDelay = false; + +const uint16_t crcTalbe[] = { + 0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, + 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400}; + +/** + * 计算Crc16 + */ +uint16_t Config::crc16(uint8_t *ptr, uint16_t len) +{ + uint16_t crc = 0xffff; + for (uint16_t i = 0; i < len; i++) + { + const uint8_t ch = *ptr++; + crc = crcTalbe[(ch ^ crc) & 15] ^ (crc >> 4); + crc = crcTalbe[((ch >> 4) ^ crc) & 15] ^ (crc >> 4); + } + return crc; +} + +void Config::resetConfig() +{ + Debug::AddInfo(PSTR("resetConfig . . . OK")); + memset(&globalConfig, 0, sizeof(GlobalConfigMessage)); + +#ifdef WIFI_SSID + strcpy(globalConfig.wifi.ssid, WIFI_SSID); +#endif +#ifdef WIFI_PASS + strcpy(globalConfig.wifi.pass, WIFI_PASS); + +#endif +#ifdef MQTT_SERVER + strcpy(globalConfig.mqtt.server, MQTT_SERVER); +#endif +#ifdef MQTT_PORT + globalConfig.mqtt.port = MQTT_PORT; +#endif +#ifdef MQTT_USER + strcpy(globalConfig.mqtt.user, MQTT_USER); +#endif +#ifdef MQTT_PASS + strcpy(globalConfig.mqtt.pass, MQTT_PASS); +#endif + globalConfig.mqtt.discovery = false; + strcpy(globalConfig.mqtt.discovery_prefix, "homeassistant"); + +#ifdef MQTT_FULLTOPIC + strcpy(globalConfig.mqtt.topic, MQTT_FULLTOPIC); +#endif +#ifdef OTA_URL + strcpy(globalConfig.http.ota_url, OTA_URL); +#endif + globalConfig.http.port = 80; + globalConfig.debug.type = 1; + + if (module) + { + module->resetConfig(); + } +} + +void Config::readConfig() +{ + uint16 len; + bool status = false; + uint16 cfg = (EEPROM.read(0) << 8 | EEPROM.read(1)); + if (cfg == GLOBAL_CFG_VERSION) + { + len = (EEPROM.read(2) << 8 | EEPROM.read(3)); + nowCrc = (EEPROM.read(4) << 8 | EEPROM.read(5)); + + if (len > GlobalConfigMessage_size) + { + len = GlobalConfigMessage_size; + } + + uint16_t crc = 0xffff; + uint8_t buffer[GlobalConfigMessage_size]; + for (uint16_t i = 0; i < len; ++i) + { + buffer[i] = EEPROM.read(i + 6); + crc = crcTalbe[(buffer[i] ^ crc) & 15] ^ (crc >> 4); + crc = crcTalbe[((buffer[i] >> 4) ^ crc) & 15] ^ (crc >> 4); + } + if (crc == nowCrc) + { + memset(&globalConfig, 0, sizeof(GlobalConfigMessage)); + pb_istream_t stream = pb_istream_from_buffer(buffer, len); + status = pb_decode(&stream, GlobalConfigMessage_fields, &globalConfig); + if (globalConfig.http.port == 0) + { + globalConfig.http.port = 80; + } + } + } + + if (!status) + { + globalConfig.debug.type = 1; + Debug::AddError(PSTR("readConfig . . . Error")); + resetConfig(); + } + else + { + if (module) + { + module->readConfig(); + } + Debug::AddInfo(PSTR("readConfig . . . OK Len: %d"), len); + } +} + +bool Config::saveConfig(bool isEverySecond) +{ + countdown = 60; + if (module) + { + module->saveConfig(isEverySecond); + } + uint8_t buffer[GlobalConfigMessage_size]; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer)); + bool status = pb_encode(&stream, GlobalConfigMessage_fields, &globalConfig); + size_t len = stream.bytes_written; + if (!status) + { + Debug::AddError(PSTR("saveConfig . . . Error")); + return false; + } + else + { + uint16_t crc = crc16(buffer, len); + if (crc == nowCrc) + { + // Debug::AddInfo(PSTR("Check Config CRC . . . Same")); + return true; + } + else + { + nowCrc = crc; + } + } + + EEPROM.write(0, GLOBAL_CFG_VERSION >> 8); + EEPROM.write(1, GLOBAL_CFG_VERSION); + + EEPROM.write(2, len >> 8); + EEPROM.write(3, len); + + EEPROM.write(4, nowCrc >> 8); + EEPROM.write(5, nowCrc); + + for (uint16_t i = 0; i < len; i++) + { + EEPROM.write(i + 6, buffer[i]); + } + EEPROM.commit(); + + Debug::AddInfo(PSTR("saveConfig . . . OK Len: %d"), len); + return true; +} + +void Config::perSecondDo() +{ + countdown--; + if (countdown == 0) + { + saveConfig(Config::isDelay ? false : true); + Config::isDelay = false; + } +} + +void Config::delaySaveConfig(uint8_t second) +{ + Config::isDelay = true; + if (countdown > second) + { + countdown = second; + } +} + +void Config::moduleReadConfig(uint16_t version, uint16_t size, const pb_field_t fields[], void *dest_struct) +{ + if (globalConfig.module_cfg.size == 0 // 没有数据 + || globalConfig.cfg_version != version // 版本不一致 + || globalConfig.module_crc != Config::crc16(globalConfig.module_cfg.bytes, globalConfig.module_cfg.size)) // crc错误 + { + Debug::AddError(PSTR("moduleReadConfig . . . Error %d %d %d"), globalConfig.cfg_version, version, globalConfig.module_cfg.size); + if (module) + { + module->resetConfig(); + } + return; + } + memset(dest_struct, 0, size); + pb_istream_t stream = pb_istream_from_buffer(globalConfig.module_cfg.bytes, globalConfig.module_cfg.size); + bool status = pb_decode(&stream, fields, dest_struct); + if (!status) // 解密失败 + { + if (module) + { + module->resetConfig(); + } + } + else + { + Debug::AddInfo(PSTR("moduleReadConfig . . . OK Len: %d"), globalConfig.module_cfg.size); + } +} + +bool Config::moduleSaveConfig(uint16_t version, uint16_t size, const pb_field_t fields[], const void *src_struct) +{ + uint8_t buffer[size]; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer)); + bool status = pb_encode(&stream, fields, src_struct); + if (status) + { + size_t len = stream.bytes_written; + uint16_t crc = Config::crc16(buffer, len); + if (crc != globalConfig.module_crc) + { + globalConfig.cfg_version = version; + globalConfig.module_crc = crc; + globalConfig.module_cfg.size = len; + memcpy(globalConfig.module_cfg.bytes, buffer, len); + //Debug::AddInfo(PSTR("moduleSaveConfig . . . OK Len: %d"), len); + } + } + return status; +} \ No newline at end of file diff --git a/lib/esp_framework/src/Debug.cpp b/lib/esp_framework/src/Debug.cpp new file mode 100644 index 0000000..b775543 --- /dev/null +++ b/lib/esp_framework/src/Debug.cpp @@ -0,0 +1,151 @@ +#include +#include +#include "Config.h" +#include "Debug.h" +#include "Rtc.h" + +uint8_t Debug::webLogIndex = 1; +char Debug::webLog[WEB_LOG_SIZE] = {'\0'}; +IPAddress Debug::ip; + +WiFiUDP Udp; +size_t Debug::strchrspn(const char *str1, int character) +{ + size_t ret = 0; + char *start = (char *)str1; + char *end = strchr(str1, character); + if (end) + ret = end - start; + return ret; +} + +void Debug::GetLog(uint8_t idx, char **entry_pp, uint16_t *len_p) +{ + char *entry_p = NULL; + size_t len = 0; + + if (idx) + { + char *it = webLog; + do + { + uint8_t cur_idx = *it; + it++; + size_t tmp = strchrspn(it, '\1'); + tmp++; // Skip terminating '\1' + if (cur_idx == idx) + { // Found the requested entry + len = tmp; + entry_p = it; + break; + } + it += tmp; + } while (it < webLog + WEB_LOG_SIZE && *it != '\0'); + } + *entry_pp = entry_p; + *len_p = len; +} + +void Debug::Syslog() +{ + if ((2 & globalConfig.debug.type) != 2 || WiFi.status() != WL_CONNECTED || globalConfig.debug.server[0] == '\0' || globalConfig.debug.port == 0) + { + return; + } + + if (!ip) + { + WiFi.hostByName(globalConfig.debug.server, ip); + } + if (Udp.beginPacket(ip, globalConfig.debug.port)) + { + char syslog_preamble[64]; // Hostname + Id + + snprintf_P(syslog_preamble, sizeof(syslog_preamble), PSTR("%s "), UID); + memmove(tmpData + strlen(syslog_preamble), tmpData, sizeof(tmpData) - strlen(syslog_preamble)); + tmpData[sizeof(tmpData) - 1] = '\0'; + memcpy(tmpData, syslog_preamble, strlen(syslog_preamble)); + Udp.write(tmpData, strlen(tmpData)); + Udp.endPacket(); + delay(1); // Add time for UDP handling (#5512) + } +} + +void Debug::AddLog(uint8_t loglevel) +{ + char mxtime[10]; // "13:45:21 " + snprintf_P(mxtime, sizeof(mxtime), PSTR("%02d:%02d:%02d "), Rtc::rtcTime.hour, Rtc::rtcTime.minute, Rtc::rtcTime.second); + + if ((1 & globalConfig.debug.type) == 1) + { + Serial.printf("%s%s\r\n", mxtime, tmpData); + } + if ((8 & globalConfig.debug.type) == 8) + { + Serial1.printf("%s%s\r\n", mxtime, tmpData); + } + + //if (Settings.webserver && (loglevel <= Settings.weblog_level)) + //{ + // Delimited, zero-terminated buffer of log lines. + // Each entry has this format: [index][log data]['\1'] + if ((4 & globalConfig.debug.type) == 4 || loglevel == LOG_LEVEL_ERROR) + { + if (!webLogIndex) + webLogIndex++; // Index 0 is not allowed as it is the end of char string + while (webLogIndex == webLog[0] || // If log already holds the next index, remove it + strlen(webLog) + strlen(tmpData) + 13 > WEB_LOG_SIZE) // 13 = web_log_index + mxtime + '\1' + '\0' + { + char *it = webLog; + it++; // Skip web_log_index + it += strchrspn(it, '\1'); // Skip log line + it++; // Skip delimiting "\1" + memmove(webLog, it, WEB_LOG_SIZE - (it - webLog)); // Move buffer forward to remove oldest log line + } + snprintf_P(webLog, sizeof(webLog), PSTR("%s%c%s%s\1"), webLog, webLogIndex++, mxtime, tmpData); + if (!webLogIndex) + webLogIndex++; // Index 0 is not allowed as it is the end of char string + } + + Syslog(); +} + +void Debug::AddLog(uint8_t loglevel, PGM_P formatP, ...) +{ + va_list arg; + va_start(arg, formatP); + vsnprintf_P(tmpData, sizeof(tmpData), formatP, arg); + va_end(arg); + + AddLog(loglevel); +} + +void Debug::AddInfo(PGM_P formatP, ...) +{ + va_list arg; + va_start(arg, formatP); + vsnprintf_P(tmpData, sizeof(tmpData), formatP, arg); + va_end(arg); + + AddLog(LOG_LEVEL_INFO); +} + +void Debug::AddDebug(PGM_P formatP, ...) +{ + va_list arg; + va_start(arg, formatP); + vsnprintf_P(tmpData, sizeof(tmpData), formatP, arg); + va_end(arg); + + AddLog(LOG_LEVEL_DEBUG); +} + +void Debug::AddError(PGM_P formatP, ...) +{ + va_list arg; + va_start(arg, formatP); + vsnprintf_P(tmpData, sizeof(tmpData), formatP, arg); + va_end(arg); + + AddLog(LOG_LEVEL_ERROR); +} diff --git a/lib/esp_framework/src/Framework.cpp b/lib/esp_framework/src/Framework.cpp new file mode 100644 index 0000000..14fbe06 --- /dev/null +++ b/lib/esp_framework/src/Framework.cpp @@ -0,0 +1,162 @@ +#include +#include "Framework.h" +#include "Module.h" +#include "Rtc.h" +#include "Http.h" +#include "Util.h" + +uint16_t Framework::rebootCount = 0; +void Framework::callback(char *topic, byte *payload, unsigned int length) +{ + String str; + for (int i = 0; i < length; i++) + { + str += (char)payload[i]; + } + + Debug::AddInfo(PSTR("Subscribe: %s payload: %s"), topic, str.c_str()); + + String topicStr = String(topic); + if (topicStr.endsWith(F("/OTA"))) + { + Http::OTA(str.endsWith(F(".bin")) ? str : OTA_URL); + } + else if (topicStr.endsWith(F("/restart"))) + { + ESP.reset(); + } + else if (module) + { + module->mqttCallback(topicStr, str); + } + + Led::led(200); +} + +void Framework::connectedCallback() +{ + Mqtt::subscribe(Mqtt::getCmndTopic(F("#"))); + Led::blinkLED(40, 8); + if (module) + { + module->mqttConnected(); + } +} + +void Framework::tickerPerSecondDo() +{ + perSecond++; + if (perSecond == 30) + { + Rtc::rtcReboot.fast_reboot_count = 0; + Rtc::rtcRebootSave(); + } + if (rebootCount == 3) + { + return; + } + Rtc::perSecondDo(); + + Config::perSecondDo(); + Mqtt::perSecondDo(); + module->perSecondDo(); +} + +void Framework::one(unsigned long baud) +{ + Rtc::rtcRebootLoad(); + Rtc::rtcReboot.fast_reboot_count++; + Rtc::rtcRebootSave(); + rebootCount = Rtc::rtcReboot.fast_reboot_count > BOOT_LOOP_OFFSET ? Rtc::rtcReboot.fast_reboot_count - BOOT_LOOP_OFFSET : 0; + + Serial.begin(baud); + EEPROM.begin(GlobalConfigMessage_size + 6); + globalConfig.debug.type = 1; +} + +void Framework::setup() +{ + Debug::AddError(PSTR("--------------------- v%s %s -------------------"), module->getModuleVersion().c_str(), Rtc::GetBuildDateAndTime().c_str()); + if (rebootCount == 1) + { + Config::readConfig(); + module->resetConfig(); + } + else if (rebootCount == 2) + { + Config::readConfig(); + module->resetConfig(); + } + else + { + Config::readConfig(); + } + if (globalConfig.uid[0] != '\0') + { + strcpy(UID, globalConfig.uid); + } + else + { + String mac = WiFi.macAddress(); + mac.replace(":", ""); + mac = mac.substring(6, 12); + sprintf(UID, "%s_%s", module->getModuleName().c_str(), mac.c_str()); + } + Util::strlowr(UID); + + Debug::AddInfo(PSTR("UID: %s"), UID); + // Debug::AddInfo(PSTR("Config Len: %d"), GlobalConfigMessage_size + 6); + + //Config::resetConfig(); + if (MQTT_MAX_PACKET_SIZE == 128) + { + Debug::AddError(PSTR("WRONG PUBSUBCLIENT LIBRARY USED PLEASE INSTALL THE ONE FROM OMG LIB FOLDER")); + } + + if (rebootCount == 3) + { + module = NULL; + + tickerPerSecond = new Ticker(); + tickerPerSecond->attach(1, tickerPerSecondDo); + + Http::begin(); + Wifi::connectWifi(); + } + else + { + Mqtt::setClient(Wifi::wifiClient); + Mqtt::mqttSetConnectedCallback(connectedCallback); + Mqtt::mqttSetLoopCallback(callback); + module->init(); + tickerPerSecond = new Ticker(); + tickerPerSecond->attach(1, tickerPerSecondDo); + Http::begin(); + Wifi::connectWifi(); + Rtc::init(); + } +} + +void Framework::loop() +{ + if (rebootCount == 3) + { + Wifi::loop(); + Http::loop(); + } + else + { + yield(); + Led::loop(); + yield(); + Mqtt::loop(); + yield(); + module->loop(); + yield(); + Wifi::loop(); + yield(); + Http::loop(); + yield(); + Rtc::loop(); + } +} diff --git a/lib/esp_framework/src/GlobalConfig.pb.c b/lib/esp_framework/src/GlobalConfig.pb.c new file mode 100644 index 0000000..25eeed2 --- /dev/null +++ b/lib/esp_framework/src/GlobalConfig.pb.c @@ -0,0 +1,82 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.3.9.4 at Sat Feb 15 11:23:30 2020. */ + +#include "GlobalConfig.pb.h" + +/* @@protoc_insertion_point(includes) */ +#if PB_PROTO_HEADER_VERSION != 30 +#error Regenerate this file with the current version of nanopb generator. +#endif + + + +const pb_field_t GlobalConfigMessage_fields[9] = { + PB_FIELD( 1, MESSAGE , SINGULAR, STATIC , FIRST, GlobalConfigMessage, wifi, wifi, &WifiConfigMessage_fields), + PB_FIELD( 2, MESSAGE , SINGULAR, STATIC , OTHER, GlobalConfigMessage, http, wifi, &HttpConfigMessage_fields), + PB_FIELD( 3, MESSAGE , SINGULAR, STATIC , OTHER, GlobalConfigMessage, mqtt, http, &MqttConfigMessage_fields), + PB_FIELD( 4, MESSAGE , SINGULAR, STATIC , OTHER, GlobalConfigMessage, debug, mqtt, &DebugConfigMessage_fields), + PB_FIELD( 5, UINT32 , SINGULAR, STATIC , OTHER, GlobalConfigMessage, cfg_version, debug, 0), + PB_FIELD( 6, UINT32 , SINGULAR, STATIC , OTHER, GlobalConfigMessage, module_crc, cfg_version, 0), + PB_FIELD( 7, BYTES , SINGULAR, STATIC , OTHER, GlobalConfigMessage, module_cfg, module_crc, 0), + PB_FIELD( 8, STRING , SINGULAR, STATIC , OTHER, GlobalConfigMessage, uid, module_cfg, 0), + PB_LAST_FIELD +}; + +const pb_field_t WifiConfigMessage_fields[8] = { + PB_FIELD( 1, STRING , SINGULAR, STATIC , FIRST, WifiConfigMessage, ssid, ssid, 0), + PB_FIELD( 2, STRING , SINGULAR, STATIC , OTHER, WifiConfigMessage, pass, ssid, 0), + PB_FIELD( 3, BOOL , SINGULAR, STATIC , OTHER, WifiConfigMessage, is_static, pass, 0), + PB_FIELD( 4, STRING , SINGULAR, STATIC , OTHER, WifiConfigMessage, ip, is_static, 0), + PB_FIELD( 5, STRING , SINGULAR, STATIC , OTHER, WifiConfigMessage, sn, ip, 0), + PB_FIELD( 6, STRING , SINGULAR, STATIC , OTHER, WifiConfigMessage, gw, sn, 0), + PB_FIELD( 7, STRING , SINGULAR, STATIC , OTHER, WifiConfigMessage, ntp, gw, 0), + PB_LAST_FIELD +}; + +const pb_field_t HttpConfigMessage_fields[5] = { + PB_FIELD( 1, UINT32 , SINGULAR, STATIC , FIRST, HttpConfigMessage, port, port, 0), + PB_FIELD( 2, STRING , SINGULAR, STATIC , OTHER, HttpConfigMessage, user, port, 0), + PB_FIELD( 3, STRING , SINGULAR, STATIC , OTHER, HttpConfigMessage, pass, user, 0), + PB_FIELD( 4, STRING , SINGULAR, STATIC , OTHER, HttpConfigMessage, ota_url, pass, 0), + PB_LAST_FIELD +}; + +const pb_field_t MqttConfigMessage_fields[10] = { + PB_FIELD( 1, STRING , SINGULAR, STATIC , FIRST, MqttConfigMessage, server, server, 0), + PB_FIELD( 2, UINT32 , SINGULAR, STATIC , OTHER, MqttConfigMessage, port, server, 0), + PB_FIELD( 3, STRING , SINGULAR, STATIC , OTHER, MqttConfigMessage, user, port, 0), + PB_FIELD( 4, STRING , SINGULAR, STATIC , OTHER, MqttConfigMessage, pass, user, 0), + PB_FIELD( 5, BOOL , SINGULAR, STATIC , OTHER, MqttConfigMessage, retain, pass, 0), + PB_FIELD( 6, STRING , SINGULAR, STATIC , OTHER, MqttConfigMessage, topic, retain, 0), + PB_FIELD( 7, BOOL , SINGULAR, STATIC , OTHER, MqttConfigMessage, discovery, topic, 0), + PB_FIELD( 8, STRING , SINGULAR, STATIC , OTHER, MqttConfigMessage, discovery_prefix, discovery, 0), + PB_FIELD( 9, UINT32 , SINGULAR, STATIC , OTHER, MqttConfigMessage, interval, discovery_prefix, 0), + PB_LAST_FIELD +}; + +const pb_field_t DebugConfigMessage_fields[4] = { + PB_FIELD( 1, UINT32 , SINGULAR, STATIC , FIRST, DebugConfigMessage, type, type, 0), + PB_FIELD( 2, STRING , SINGULAR, STATIC , OTHER, DebugConfigMessage, server, type, 0), + PB_FIELD( 3, UINT32 , SINGULAR, STATIC , OTHER, DebugConfigMessage, port, server, 0), + PB_LAST_FIELD +}; + + +/* Check that field information fits in pb_field_t */ +#if !defined(PB_FIELD_32BIT) +/* If you get an error here, it means that you need to define PB_FIELD_32BIT + * compile-time option. You can do that in pb.h or on compiler command line. + * + * The reason you need to do this is that some of your messages contain tag + * numbers or field sizes that are larger than what can fit in 8 or 16 bit + * field descriptors. + */ +PB_STATIC_ASSERT((pb_membersize(GlobalConfigMessage, wifi) < 65536 && pb_membersize(GlobalConfigMessage, http) < 65536 && pb_membersize(GlobalConfigMessage, mqtt) < 65536 && pb_membersize(GlobalConfigMessage, debug) < 65536 && pb_membersize(GlobalConfigMessage, module_cfg) < 65536), YOU_MUST_DEFINE_PB_FIELD_32BIT_FOR_MESSAGES_GlobalConfigMessage_WifiConfigMessage_HttpConfigMessage_MqttConfigMessage_DebugConfigMessage) +#endif + +#if !defined(PB_FIELD_16BIT) && !defined(PB_FIELD_32BIT) +#error Field descriptor for GlobalConfigMessage.module_cfg is too large. Define PB_FIELD_16BIT to fix this. +#endif + + +/* @@protoc_insertion_point(eof) */ diff --git a/lib/esp_framework/src/Http.cpp b/lib/esp_framework/src/Http.cpp new file mode 100644 index 0000000..5bb6048 --- /dev/null +++ b/lib/esp_framework/src/Http.cpp @@ -0,0 +1,953 @@ +#include +#include +#include +#include +#include "StreamString.h" +#include +#include "Http.h" +#include "Module.h" +#include "Rtc.h" + +ESP8266WebServer *Http::server; +bool Http::isBegin = false; +String Http::updaterError; + +void Http::handleRoot() +{ + if (captivePortal()) + { + return; + } + if (!checkAuth()) + { + return; + } + + server->setContentLength(CONTENT_LENGTH_UNKNOWN); + server->send(200, F("text/html"), ""); + String radioJs = ""); + + page += F("
"); + page += F("

{title}

"); + page.replace(F("{title}"), module ? module->getModuleCNName() : F("修复模式")); + + page += F(""); + server->sendContent(page); + + page = F("
"); + + // TAB 1 Start + uint8_t mode = WiFi.getMode(); + page += F("
"); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F("
WiFi状态
主机名{UID}
WiFi模式"); + if (mode == WIFI_STA) + { + page += F("STA"); + } + else if (mode == WIFI_AP) + { + page += F("AP"); + } + else if (mode == WIFI_AP_STA) + { + page += F("AP STA"); + } + page += F("
SSID{SSID}
RSSI{RSSI}dBm
开机时间{uptime}
空闲内存{free_mem} kB
IP地址{localIP}
DHCP{DHCP}
"); + page += F("
"); + page.replace(F("{UID}"), UID); + page.replace(F("{SSID}"), WiFi.SSID()); + page.replace(F("{RSSI}"), String(WiFi.RSSI())); + page.replace(F("{uptime}"), Rtc::msToHumanString(millis())); + page.replace(F("{free_mem}"), String(ESP.getFreeHeap() / 1024)); + page.replace(F("{localIP}"), WiFi.localIP().toString()); + page.replace(F("{DHCP}"), (!globalConfig.wifi.is_static ? F("DHCP") : F("静态IP"))); + // TAB 1 End + + server->sendContent(page); + + // TAB 2 Start + page = F("
"); + page += F("
"); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F("
WiFi名称信号
WiFi名称
WiFi密码
"); + page += F(""); + if (!WiFi.isConnected()) + { + radioJs += "scanWifi();"; + } + + page += F("
"); + page += F(""); + page += F(""); + radioJs += F("setRadioValue('dhcp', '{v}');"); + radioJs.replace(F("{v}"), globalConfig.wifi.is_static ? F("2") : F("1")); + + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F("
WIFI高级设置
DHCP"); + page += F("    "); + page += F(""); + page += F("
静态IP
子网掩码
网关
"); + page += F(""); + page.replace(F("{ip}"), globalConfig.wifi.ip); + page.replace(F("{sn}"), globalConfig.wifi.sn); + page.replace(F("{gw}"), globalConfig.wifi.gw); + + page += F("
"); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F("
MQTT设置
地址
端口    0为不启动mqtt
用户名
密码
主题
心跳上报间隔 秒  0为不上报
retain"); + page += F("    "); + page += F("
除非你知道它是干嘛的。"); + page += F("
状态{mqttconnected}
"); + page.replace(F("{server}"), globalConfig.mqtt.server); + page.replace(F("{port}"), String(globalConfig.mqtt.port)); + page.replace(F("{user}"), globalConfig.mqtt.user); + page.replace(F("{pass}"), globalConfig.mqtt.pass); + page.replace(F("{topic}"), globalConfig.mqtt.topic); + page.replace(F("{interval}"), String(globalConfig.mqtt.interval)); + radioJs += F("setRadioValue('retain', '{v}');"); + radioJs.replace(F("{v}"), globalConfig.mqtt.retain ? F("1") : F("0")); + page.replace(F("{mqttconnected}"), (Mqtt::mqttClient.connected() ? F("已连接") : F("未连接"))); + + page += F("
"); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + if (globalConfig.mqtt.discovery) + { + radioJs += F("id('discovery_btn').setAttribute('class', 'btn-danger');id('discovery_btn').innerHTML='关闭MQTT自动发现';"); + } + page += F("
MQTT自动发现
自发现状态{discovery}
自发现前缀
"); + page += F("
"); + page.replace(F("{discovery}"), globalConfig.mqtt.discovery ? F("已启动") : F("未启动")); + page.replace(F("{prefix}"), globalConfig.mqtt.discovery_prefix); + // TAB 2 End + + // TAB 3 Start + page += F("
"); + server->sendContent(page); + + if (module) + { + module->httpHtml(server); + } + + page = F("
"); + page += F(""); + page += F(""); + page += F(""); + page.replace(F("{UID}"), UID); + if ((1 & globalConfig.debug.type) == 1) + { + radioJs += F("setRadioValue('log_serial', '1');"); + } + if ((2 & globalConfig.debug.type) == 2) + { + radioJs += F("setRadioValue('log_syslog', '1');"); + } + if ((4 & globalConfig.debug.type) == 4) + { + radioJs += F("setRadioValue('log_web', '1');"); + } + if ((8 & globalConfig.debug.type) == 8) + { + radioJs += F("setRadioValue('log_serial1', '1');"); + } + + page += F(""); + page.replace(F("{server}"), globalConfig.debug.server); + page.replace(F("{port}"), String(globalConfig.debug.port)); + + page += F(""); + + page += F(""); + page += F("
模块设置
主机名 具有唯一性,留空默认
日志输出"); + page += F("    "); + page += F("    "); + page += F("    "); + page += F("    "); + page += F("
syslog服务器"); + page += F(" : "); + page += F("
NTP服务器"); + page += F(" 建议在获取时间失败时才填写"); + page.replace(F("{ntp}"), globalConfig.wifi.ntp); + page += F("
"); + + page += F("
"); + page += F(""); + page += F(""); + page += F("
"); + page += F("
"); + // TAB 3 End + server->sendContent(page); + + // TAB 4 Start + page = F("
"); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F("
硬件参数
ESP芯片ID{getChipId}
Flash芯片 ID{getFlashChipId}
Flash大小{getFlashChipRealSize} kB
固件Flash大小{getFlashChipSize} kB
固件大小{getSketchSize} kB
空闲程序空间{getFreeSketchSpace} kB
内核和SDK版本" ARDUINO_ESP8266_RELEASE "/{getSdkVersion}
重启原因{resetReason}
MAC地址{macAddress}
"); + page.replace(F("{getChipId}"), String(ESP.getChipId())); + page.replace(F("{getFlashChipId}"), String(ESP.getFlashChipId())); + page.replace(F("{getFlashChipSize}"), String(ESP.getFlashChipSize() / 1024)); + page.replace(F("{getFlashChipRealSize}"), String(ESP.getFlashChipRealSize() / 1024)); + page.replace(F("{getSketchSize}"), String(ESP.getSketchSize() / 1024)); + page.replace(F("{getFreeSketchSpace}"), String(ESP.getFreeSketchSpace() / 1024)); + page.replace(F("{getSdkVersion}"), ESP.getSdkVersion()); + page.replace(F("{macAddress}"), WiFi.macAddress()); + page.replace(F("{resetReason}"), ESP.getResetReason()); + + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F(""); + page += F("
固件升级
当前版本v{v}
编译时间{v1}
选择文件

"); + page += F(""); + page += F("
OTA更新
OTA地址
"); + page += F("
"); + page.replace(F("{v}"), module ? module->getModuleVersion() : F("0")); + page.replace(F("{v1}"), Rtc::GetBuildDateAndTime()); + page.replace(F("{ota_url}"), globalConfig.http.ota_url); + // TAB 4 End + server->sendContent(page); + + // TAB 5 Start + page = F("
"); + page += F("
"); + page += F(""); + page += F("
"); + // TAB 5 End + + page += F("
"); + radioJs += F(""); + + page += radioJs; + + page += F("
"); + + server->sendContent(page); +} + +void Http::handleMqtt() +{ + if (!checkAuth()) + { + return; + } + String topic = server->arg(F("mqtt_topic")); + if (topic.length() == 0) + { + Http::server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"MQTT主题不能为空\"}")); + return; + } + if (topic.indexOf("%prefix%/") == 0) + { + Http::server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"MQTT主题必须包含【%prefix%/】\"}")); + return; + } + strcpy(globalConfig.mqtt.server, server->arg(F("mqtt_server")).c_str()); + globalConfig.mqtt.port = server->arg(F("mqtt_port")).toInt(); + globalConfig.mqtt.retain = server->arg(F("retain")) == F("1"); + strcpy(globalConfig.mqtt.user, server->arg(F("mqtt_username")).c_str()); + strcpy(globalConfig.mqtt.pass, server->arg(F("mqtt_password")).c_str()); + strcpy(globalConfig.mqtt.topic, topic.c_str()); + globalConfig.mqtt.interval = server->arg(F("interval")).toInt(); + Config::saveConfig(); + Mqtt::setTopic(); + + if (Mqtt::mqttClient.connected()) + { + Mqtt::mqttClient.disconnect(); + } + + if (Mqtt::mqttConnect()) + { + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"设置MQTT服务器成功,已连接。\",\"data\":{\"mqttconnected\":\"已连接\"}}")); + } + else + { + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"设置MQTT服务器成功,未连接。\",\"data\":{\"mqttconnected\":\"未连接\"}}")); + } +} + +void Http::handledhcp() +{ + if (!checkAuth()) + { + return; + } + String ip = server->arg(F("static_ip")); + String netmask = server->arg(F("static_netmask")); + String gateway = server->arg(F("static_gateway")); + if (!Wifi::isIp(ip)) + { + Http::server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"IP地址错误\"}")); + return; + } + if (!Wifi::isIp(netmask)) + { + Http::server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"掩码地址错误\"}")); + return; + } + if (!Wifi::isIp(gateway)) + { + Http::server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"网关地址错误\"}")); + return; + } + + IPAddress static_ip; + IPAddress static_sn; + IPAddress static_gw; + static_ip.fromString(ip); + static_sn.fromString(netmask); + static_gw.fromString(gateway); + + if (!(static_ip.isV4() && static_sn.isV4() && (!static_gw.isSet() || static_gw.isV4()))) + { + Http::server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"IP地址或者网关错误\"}")); + return; + } + + if ((static_ip.v4() & static_sn.v4()) != (static_gw.v4() & static_sn.v4())) + { + Http::server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"网段错误\"}")); + return; + } + + bool old = globalConfig.wifi.is_static; + globalConfig.wifi.is_static = server->arg(F("dhcp")).equals(F("2")); + strcpy(globalConfig.wifi.ip, ip.c_str()); + strcpy(globalConfig.wifi.sn, netmask.c_str()); + strcpy(globalConfig.wifi.gw, gateway.c_str()); + Config::saveConfig(); + + if (old != globalConfig.wifi.is_static) + { + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"设置DHCP信息成功,重启后生效\"}")); + } + else + { + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"设置DHCP信息成功\"}")); + } +} + +void Http::handleScanWifi() +{ + if (!checkAuth()) + { + return; + } + int n = WiFi.scanNetworks(); + if (n == 0) + { + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"\",\"data\":{\"list\":[]}}")); + //Http::server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"找不到网络,请重新试试。\"}")); + return; + } + + //sort networks + int indices[n]; + for (int i = 0; i < n; i++) + { + indices[i] = i; + } + + // RSSI排序 + for (int i = 0; i < n; i++) + { + for (int j = i + 1; j < n; j++) + { + if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) + { + std::swap(indices[i], indices[j]); + } + } + } + + // 删除重复项(必须对RSSI进行排序) + String cssid; + for (int i = 0; i < n; i++) + { + if (indices[i] == -1) + continue; + cssid = WiFi.SSID(indices[i]); + for (int j = i + 1; j < n; j++) + { + if (cssid == WiFi.SSID(indices[j])) + { + indices[j] = -1; // set dup aps to index -1 + } + } + } + + String data = ""; + for (int i = 0; i < n; i++) + { + if (indices[i] == -1) + continue; // skip dups + int RSSI = WiFi.RSSI(indices[i]); + int quality; + if (RSSI <= -100) + { + quality = 0; + } + else if (RSSI >= -50) + { + quality = 100; + } + else + { + quality = 2 * (RSSI + 100); + } + int _minimumQuality = -1; + if (_minimumQuality == -1 || _minimumQuality < quality) + { + data += ",{\"name\":\"" + WiFi.SSID(indices[i]) + "\",\"rssi\":\"" + RSSI + "\",\"quality\":" + quality + ",\"type\":" + WiFi.encryptionType(indices[i]) + "}"; + } + } + + Http::server->send(200, F("text/html"), "{\"code\":1,\"msg\":\"\",\"data\":{\"list\":[" + data.substring(1) + "]}}"); +} + +void Http::handleWifi() +{ + if (!checkAuth()) + { + return; + } + String wifi = server->arg(F("wifi_ssid")); + if (wifi == "") + { + Http::server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"WiFi名称不能为空。\"}")); + return; + } + String password = server->arg(F("wifi_password")); + + if (WiFi.getMode() == WIFI_STA) + { + strcpy(globalConfig.wifi.ssid, wifi.c_str()); + strcpy(globalConfig.wifi.pass, password.c_str()); + Config::saveConfig(); + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"设置WiFi信息成功,重启模块(手动)使用新的Wifi信息连接。\"}")); + } + else + { + Wifi::tryConnect(wifi, password); + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"尝试将ESP连接到网络。 如果失败,请重新连接到AP再试一次。\"}")); + } +} + +void Http::handleDiscovery() +{ + if (!checkAuth()) + { + return; + } + strcpy(globalConfig.mqtt.discovery_prefix, server->arg(F("discovery_prefix")).c_str()); + globalConfig.mqtt.discovery = !globalConfig.mqtt.discovery; + Config::saveConfig(); + + if (module) + { + module->mqttDiscovery(globalConfig.mqtt.discovery); + } + if (globalConfig.mqtt.discovery) + { + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"已经打开MQTT自发现。\",\"data\":{\"discovery\":1}}")); + } + else + { + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"已经关闭MQTT自发现。\",\"data\":{\"discovery\":0}}")); + } +} + +void Http::handleRestart() +{ + if (!checkAuth()) + { + return; + } + Config::saveConfig(); + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"设备正在重启 . . .\"}")); + delay(200); + + Led::blinkLED(400, 4); + ESP.restart(); +} + +void Http::handleReset() +{ + if (!checkAuth()) + { + return; + } + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"正在重置模块 . . . 设备将会重启。\"}")); + delay(200); + + Led::blinkLED(400, 4); + + Config::resetConfig(); + Config::saveConfig(); + ESP.restart(); +} + +void Http::handleOTA() +{ + if (!checkAuth()) + { + return; + } + strcpy(globalConfig.http.ota_url, server->arg(F("ota_url")).c_str()); + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"如果成功后设备会重启 . . . \"}")); + Http::OTA(String(globalConfig.http.ota_url)); +} + +void Http::handleNotFound() +{ + if (captivePortal()) + { + return; + } + String message = F("File Not Found\n\n"); + message += F("URI: "); + message += server->uri(); + message += F("\nMethod: "); + message += (server->method() == HTTP_GET) ? F("GET") : F("POST"); + message += F("\nArguments: "); + message += server->args(); + message += F("\n"); + for (uint8_t i = 0; i < server->args(); i++) + { + message += " " + server->argName(i) + ": " + server->arg(i) + "\n"; + } + server->sendHeader(F("Cache-Control"), F("no-cache, no-store, must-revalidate")); + server->sendHeader(F("Pragma"), F("no-cache")); + server->sendHeader(F("Expires"), F("-1")); + server->sendHeader(F("Content-Length"), String(message.length())); + server->send(404, F("text/plain"), message); +} + +void Http::handleGetStatus() +{ + if (!checkAuth()) + { + return; + } + bool cflg = true; + uint8_t counter = 0; + if (server->hasArg(F("i"))) + { + counter = server->arg(F("i")).toInt(); + } + + server->setContentLength(CONTENT_LENGTH_UNKNOWN); + server->send(200, F("text/html"), ""); + String data = F("{\"code\":1,\"msg\":\"\",\"data\":{"); + data += F("\"mqttconnected\":\""); + data += Mqtt::mqttClient.connected() ? F("已连接") : F("未连接"); + + data += F("\",\"discovery\":"); + data += globalConfig.mqtt.discovery ? 1 : 0; + + data += F(",\"uptime\":\""); + data += Rtc::msToHumanString(millis()); + + data += F("\",\"ip\":\""); + if (Wifi::configPortalStart == 0 && WiFi.isConnected()) + { + data += WiFi.localIP().toString(); + } + else + { + data += F(""); + } + + data += F("\",\"free_mem\":"); + data += ESP.getFreeHeap() / 1024; + + if (module) + { + String tmp = module->httpGetStatus(server); + if (tmp.length() > 0) + { + data += F(","); + data += tmp; + } + } + + data += F(",\"logindex\":"); + data += Debug::webLogIndex; + + data += F(",\"log\":\""); + server->sendContent(data); + + if (counter != Debug::webLogIndex) + { + if (!counter) + { + counter = Debug::webLogIndex; + cflg = false; + } + do + { + char *tmp; + uint16_t len; + Debug::GetLog(counter, &tmp, &len); + if (len) + { + if (cflg) + { + server->sendContent("\\n"); + } + + size_t j = 0; + for (size_t i = 0; i < len - 1; i++) + { + char each = tmp[i]; + if (each == '\\' || each == '"') + { + tmpData[j++] = '\\'; + tmpData[j++] = each; + } + else if (each == '\b') + { + tmpData[j++] = '\\'; + tmpData[j++] = 'b'; + } + else if (each == '\f') + { + tmpData[j++] = '\\'; + tmpData[j++] = 'f'; + } + else if (each == '\n') + { + tmpData[j++] = '\\'; + tmpData[j++] = 'n'; + } + else if (each == '\r') + { + tmpData[j++] = '\\'; + tmpData[j++] = 'r'; + } + else if (each == '\t') + { + tmpData[j++] = '\\'; + tmpData[j++] = 't'; + } + else + { + tmpData[j++] = each; + } + } + tmpData[j++] = '\0'; + + server->sendContent(tmpData); + cflg = true; + } + counter++; + if (!counter) + { + counter++; + } // Skip log index 0 as it is not allowed + } while (counter != Debug::webLogIndex); + } + + server->sendContent(F("\"}}")); +} + +void Http::handleUpdate() +{ + // handler for the /update form POST (once file upload finishes) + Http::server->on("/update", HTTP_POST, [&]() { + if (!checkAuth()) + { + return; + } + if (Update.hasError()) + { + Debug::AddError(PSTR("Update error: %s"), Http::updaterError.c_str()); + Http::server->send(200, F("text/html"), String(F("Update error: ")) + Http::updaterError); + } + else + { + Config::saveConfig(); + Http::server->client().setNoDelay(true); + Http::server->send_P(200, PSTR("text/html"), PSTR("升级成功!正在重启 . . .")); + delay(100); + Http::server->client().stop(); + ESP.restart(); + } }, [&]() { + HTTPUpload &upload = Http::server->upload(); + if (upload.status == UPLOAD_FILE_START) + { + Http::updaterError = String(); + if (globalConfig.http.user[0] != 0 && globalConfig.http.pass[0] != 0 && server->client().localIP().toString() != "192.168.4.1" && !server->authenticate(globalConfig.http.user, globalConfig.http.pass)) + { + Debug::AddInfo(PSTR("Unauthenticated Update")); + return; + } + WiFiUDP::stopAll(); + Debug::AddInfo(PSTR("Update: %s"), upload.filename.c_str()); + uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; + if (!Update.begin(maxSketchSpace, U_FLASH))//start with max available size + { + StreamString str; + Update.printError(str); + Http::updaterError = str.c_str(); + } + } + else if (upload.status == UPLOAD_FILE_WRITE && !Http::updaterError.length()) + { + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) + { + StreamString str; + Update.printError(str); + Http::updaterError = str.c_str(); + } + } + else if (upload.status == UPLOAD_FILE_END && !Http::updaterError.length()) + { + if (Update.end(true)) + { + Debug::AddInfo(PSTR("Update Success: %u Rebooting..."), upload.totalSize); + } + else + { + StreamString str; + Update.printError(str); + Http::updaterError = str.c_str(); + } + } + else if (upload.status == UPLOAD_FILE_ABORTED) + { + Update.end(); + Debug::AddInfo(PSTR("Update was aborted")); + } + delay(0); }); +} + +void Http::begin() +{ + if (isBegin) + { + return; + } + isBegin = true; + server = new ESP8266WebServer(); + + server->on(F("/"), handleRoot); + server->on(F("/mqtt"), handleMqtt); + server->on(F("/dhcp"), handledhcp); + server->on(F("/scan_wifi"), handleScanWifi); + server->on(F("/wifi"), handleWifi); + server->on(F("/discovery"), handleDiscovery); + server->on(F("/restart"), handleRestart); + server->on(F("/reset"), handleReset); + server->on(F("/module_setting"), handleModuleSetting); + server->on(F("/ota"), handleOTA); + server->on(F("/get_status"), handleGetStatus); + server->onNotFound(handleNotFound); + handleUpdate(); + + if (module) + { + module->httpAdd(server); + } + MDNS.begin(UID); + server->begin(globalConfig.http.port); + Debug::AddInfo(PSTR("HTTP server started port: %d"), globalConfig.http.port); +} + +void Http::stop() +{ + if (!isBegin) + { + return; + } + server->stop(); + Debug::AddInfo(PSTR("HTTP server stoped")); +} + +void Http::loop() +{ + if (isBegin) + { + server->handleClient(); + MDNS.update(); + } +} + +bool Http::captivePortal() +{ + if (!Wifi::isIp(server->hostHeader())) + { + //Debug::AddInfo(PSTR("Request redirected to captive portal")); + server->sendHeader(F("Location"), String(F("http://")) + server->client().localIP().toString(), true); + server->send(302, F("text/plain"), ""); // Empty content inhibits Content-length header so we have to close the socket ourselves. + server->client().stop(); // Stop is needed because we sent no content length + return true; + } + return false; +} + +void Http::handleModuleSetting() +{ + if (!checkAuth()) + { + return; + } + if (Http::server->hasArg(F("log_serial")) || Http::server->hasArg(F("log_serial1")) || Http::server->hasArg(F("log_syslog")) || Http::server->hasArg(F("log_web"))) + { + int t = 0; + if (Http::server->arg(F("log_serial")).equals(F("1"))) + { + t = t | 1; + } + if (Http::server->arg(F("log_serial1")).equals(F("1"))) + { + t = t | 8; + } + if (Http::server->arg(F("log_web")).equals(F("1"))) + { + t = t | 4; + } + + String log_syslog = Http::server->arg(F("log_syslog")); + if (log_syslog.equals(F("1"))) + { + t = t | 2; + String log_syslog_host = Http::server->arg(F("log_syslog_host")); + String log_syslog_port = Http::server->arg(F("log_syslog_port")); + if (log_syslog_host.length() == 0) + { + Http::server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"syslog服务器不能为空\"}")); + return; + } + strcpy(globalConfig.debug.server, log_syslog_host.c_str()); + globalConfig.debug.port = log_syslog_port.toInt(); + WiFi.hostByName(globalConfig.debug.server, Debug::ip); + } + + if (Http::server->arg(F("log_serial1")).equals(F("1"))) + { + Serial1.begin(115200); + } + globalConfig.debug.type = t; + } + + String ntp = Http::server->arg(F("ntp")); + if ((globalConfig.wifi.ntp[0] == '\0' && ntp.length() > 0) || (globalConfig.wifi.ntp[0] != '\0' && ntp.length() == 0)) + { + strcpy(globalConfig.wifi.ntp, ntp.c_str()); + Rtc::init(); + } + else + { + strcpy(globalConfig.wifi.ntp, ntp.c_str()); + } + + String uid = Http::server->arg(F("uid")); + strcpy(globalConfig.uid, uid.c_str()); + Config::saveConfig(); + if (uid.length() == 0 || strncmp(globalConfig.uid, UID, uid.length()) != 0) + { + if (globalConfig.mqtt.discovery && module) + { + module->mqttDiscovery(false); + } + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"修改了重要配置 . . . 正在重启中。\"}")); + Led::blinkLED(400, 4); + ESP.restart(); + } + else + { + Http::server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"已经修改成功\"}")); + } +} + +bool Http::checkAuth() +{ + if (globalConfig.http.user[0] != 0 && globalConfig.http.pass[0] != 0 && server->client().localIP().toString() != "192.168.4.1") + { + if (!server->authenticate(globalConfig.http.user, globalConfig.http.pass)) + { + server->requestAuthentication(); + return false; + } + } + return true; +} + +void Http::OTA(String url) +{ + if (url.indexOf(F("%04d")) != -1) + { + url.replace("%04d", String(ESP.getChipId() & 0x1fff)); + } + else if (url.indexOf(F("%d")) != -1) + { + url.replace("%d", String(ESP.getChipId())); + } + url.replace(F("%hostname%"), UID); + url.replace(F("%module%"), module ? module->getModuleName() : ""); + + Config::saveConfig(); + Debug::AddInfo(PSTR("OTA Url: %s"), url.c_str()); + Led::blinkLED(200, 5); + WiFiClient OTAclient; + if (ESPhttpUpdate.update(OTAclient, url, (module ? module->getModuleVersion() : "")) == HTTP_UPDATE_FAILED) + { + Debug::AddError(PSTR("HTTP_UPDATE_FAILD Error (%d): %s"), ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str()); + } +} diff --git a/lib/esp_framework/src/Led.cpp b/lib/esp_framework/src/Led.cpp new file mode 100644 index 0000000..56e1bee --- /dev/null +++ b/lib/esp_framework/src/Led.cpp @@ -0,0 +1,113 @@ +#include +#include "Led.h" +#include "Module.h" + +Ticker *Led::ledTicker; +Ticker *Led::ledTicker2; +uint8_t Led::io = 99; +uint8_t Led::light; +uint8_t Led::ledType = 0; +bool Led::isOn = false; + +void Led::init(uint8_t _io, uint8_t _light) +{ + io = _io; + light = _light; + pinMode(io, OUTPUT); + + Led::ledType = 0; + ledTicker = new Ticker(); + ledTicker2 = new Ticker(); + + off(); +} + +void Led::loop() +{ + if (io == 99) + { + return; + } + if (module && module->moduleLed()) + { + if (ledTicker->active()) + { + ledTicker->detach(); + } + Led::ledType = 3; + } + else if (WiFi.status() != WL_CONNECTED) + { + if (Led::ledType != 0) + { + Led::ledType = 0; + ledTicker->attach(0.2, []() { toggle(); }); + } + } + else if (!Mqtt::mqttClient.connected()) + { + if (Led::ledType != 1) + { + Led::ledType = 1; + ledTicker->attach(0.3, []() { toggle(); }); + } + } + else + { + if (Led::ledType != 2) + { + Led::ledType = 2; + ledTicker->attach(5, led, 200); + } + } +} + +void Led::on() +{ + if (io != 99 && !isOn) + { + isOn = true; + digitalWrite(io, light); + } +} + +void Led::off() +{ + if (io != 99 && isOn) + { + isOn = false; + digitalWrite(io, !light); + } +} + +void Led::toggle() +{ + isOn ? off() : on(); +} + +void Led::led(int ms) +{ + if (io != 99) + { + on(); + ledTicker2->once_ms(ms, []() { off(); }); + } +} + +void Led::blinkLED(int duration, int n) +{ + if (io == 99) + { + return; + } + for (int i = 0; i < n; i++) + { + on(); + delay(duration); + off(); + if (n != i + 1) + { + delay(duration); + } + } +} diff --git a/lib/esp_framework/src/Mqtt.cpp b/lib/esp_framework/src/Mqtt.cpp new file mode 100644 index 0000000..521c948 --- /dev/null +++ b/lib/esp_framework/src/Mqtt.cpp @@ -0,0 +1,210 @@ +#include +#include "Mqtt.h" +#include "Module.h" + +String Mqtt::topicCmnd; +String Mqtt::topicStat; +String Mqtt::topicTele; +uint8_t Mqtt::operationFlag = 0; +PubSubClient Mqtt::mqttClient; +uint32_t Mqtt::lastReconnectAttempt = 0; // 最后尝试重连时间 +uint32_t Mqtt::kMqttReconnectTime = 30000; // 重新连接尝试之间的延迟(ms) +std::function Mqtt::connectedcallback = NULL; + +bool Mqtt::mqttConnect() +{ + if (WiFi.status() != WL_CONNECTED) + { + Debug::AddInfo(PSTR("wifi disconnected")); + return false; + } + if (globalConfig.mqtt.port == 0) + { + Debug::AddInfo(PSTR("no set mqtt info")); + return false; + } + if (mqttClient.connected()) + { + return true; + } + + Debug::AddInfo(PSTR("client mqtt not connected, trying to connect to %s:%d Broker"), globalConfig.mqtt.server, globalConfig.mqtt.port); + mqttClient.setServer(globalConfig.mqtt.server, globalConfig.mqtt.port); + + if (mqttClient.connect(UID, globalConfig.mqtt.user, globalConfig.mqtt.pass, getTeleTopic(F("availability")).c_str(), 0, true, "offline")) + { + Debug::AddInfo(PSTR("successful client mqtt connection")); + if (connectedcallback != NULL) + { + connectedcallback(); + } + } + else + { + Debug::AddInfo(PSTR("Connecting to %s:%d Broker . . failed, rc=%d"), globalConfig.mqtt.server, globalConfig.mqtt.port, mqttClient.state()); + } + + return mqttClient.connected(); +} + +void Mqtt::doReportHeartbeat() +{ + char message[250]; + sprintf(message, "{\"UID\":\"%s\",\"SSID\":\"%s\",\"RSSI\":\"%s\",\"Version\":\"%s\",\"ip\":\"%s\",\"mac\":\"%s\",\"freeMem\":%d,\"uptime\":%d}", + UID, WiFi.SSID().c_str(), String(WiFi.RSSI()).c_str(), (module ? module->getModuleVersion().c_str() : "0"), WiFi.localIP().toString().c_str(), WiFi.macAddress().c_str(), ESP.getFreeHeap(), millis() / 1000); + //Debug::AddInfo(PSTR("%s"), message); + publish(getTeleTopic(F("HEARTBEAT")), message); +} + +void Mqtt::availability() +{ + publish(getTeleTopic(F("availability")), "online", true); +} + +void Mqtt::perSecondDo() +{ + bitSet(operationFlag, 0); +} + +void Mqtt::loop() +{ + if (WiFi.status() != WL_CONNECTED || globalConfig.mqtt.port == 0) + { + return; + } + uint32_t now = millis(); + if (!mqttClient.connected()) + { + if (now - lastReconnectAttempt > kMqttReconnectTime || lastReconnectAttempt == 0) + { + lastReconnectAttempt = now; + if (mqttConnect()) + { + lastReconnectAttempt = 0; + availability(); + if (globalConfig.mqtt.interval > 0) + { + doReportHeartbeat(); + } + } + } + } + else + { + mqttClient.loop(); + if (bitRead(operationFlag, 0)) + { + bitClear(operationFlag, 0); + if (globalConfig.mqtt.interval > 0 && (perSecond % globalConfig.mqtt.interval) == 0) + { + doReportHeartbeat(); + } + if (perSecond % 3609 == 0) + { + availability(); + } + } + } +} + +void Mqtt::setTopic() +{ + topicCmnd = getTopic(0, ""); + topicStat = getTopic(1, ""); + topicTele = getTopic(2, ""); +} + +String Mqtt::getCmndTopic(String topic) +{ + return topicCmnd + topic; +} + +String Mqtt::getStatTopic(String topic) +{ + return topicStat + topic; +} +String Mqtt::getTeleTopic(String topic) +{ + return topicTele + topic; +} + +void Mqtt::mqttSetLoopCallback(MQTT_CALLBACK_SIGNATURE) +{ + mqttClient.setCallback(callback); +} + +void Mqtt::mqttSetConnectedCallback(MQTT_CONNECTED_CALLBACK_SIGNATURE) +{ + Mqtt::connectedcallback = connectedcallback; +} + +PubSubClient &Mqtt::setClient(Client &client) +{ + setTopic(); + return mqttClient.setClient(client); +} +bool Mqtt::publish(String topic, const char *payload) +{ + return mqttClient.publish(topic.c_str(), payload); +} + +bool Mqtt::publish(String topic, const char *payload, bool retained) +{ + return mqttClient.publish(topic.c_str(), payload, retained); +} + +bool Mqtt::publish(const char *topic, const char *payload) +{ + return mqttClient.publish(topic, payload); +} +bool Mqtt::publish(const char *topic, const char *payload, bool retained) +{ + return mqttClient.publish(topic, payload, retained); +} +bool Mqtt::publish(const char *topic, const uint8_t *payload, unsigned int plength) +{ + return mqttClient.publish(topic, payload, plength); +} +bool Mqtt::publish(const char *topic, const uint8_t *payload, unsigned int plength, bool retained) +{ + return mqttClient.publish(topic, payload, plength, retained); +} +bool Mqtt::publish_P(const char *topic, const char *payload, bool retained) +{ + return mqttClient.publish_P(topic, payload, retained); +} +bool Mqtt::publish_P(const char *topic, const uint8_t *payload, unsigned int plength, bool retained) +{ + return mqttClient.publish_P(topic, payload, plength, retained); +} + +bool Mqtt::subscribe(String topic) +{ + return mqttClient.subscribe(topic.c_str()); +} +bool Mqtt::subscribe(String topic, uint8_t qos) +{ + return mqttClient.subscribe(topic.c_str(), qos); +} +bool Mqtt::unsubscribe(String topic) +{ + return mqttClient.unsubscribe(topic.c_str()); +} + +String Mqtt::getTopic(uint8_t prefix, String subtopic) +{ + // 0: Cmnd 1:Stat 2:Tele + String fulltopic = String(globalConfig.mqtt.topic); + if ((0 == prefix) && (-1 == fulltopic.indexOf(F("%prefix%")))) + { + fulltopic += F("/%prefix%"); // Need prefix for commands to handle mqtt topic loops + } + fulltopic.replace(F("%prefix%"), (prefix == 0 ? F("cmnd") : ((prefix == 1 ? F("stat") : F("tele"))))); + fulltopic.replace(F("%hostname%"), UID); + fulltopic.replace(F("%module%"), module ? module->getModuleName() : F("module")); + fulltopic.replace(F("#"), ""); + fulltopic.replace(F("//"), "/"); + if (!fulltopic.endsWith(F("/"))) + fulltopic += F("/"); + return fulltopic + subtopic; +} diff --git a/lib/esp_framework/src/Rtc.cpp b/lib/esp_framework/src/Rtc.cpp new file mode 100644 index 0000000..3bbff2d --- /dev/null +++ b/lib/esp_framework/src/Rtc.cpp @@ -0,0 +1,236 @@ +#include +#include "Rtc.h" +#include "sntp.h" +#include "Debug.h" + +RtcReboot Rtc::rtcReboot; +uint32_t Rtc::rtcRebootCrc = 0; +TIME_T Rtc::rtcTime; +uint32_t Rtc::utcTime; +uint8_t Rtc::operationFlag = 0; +static const uint8_t kDaysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // API starts months from 1, this array starts from 0 +static const char kMonthNamesEnglish[] = "JanFebMarAprMayJunJulAugSepOctNovDec"; + +String Rtc::GetBuildDateAndTime() +{ + // "2017-03-07T11:08:02" - ISO8601:2004 + char bdt[21]; + char *p; + char mdate[] = __DATE__; // "Mar 7 2017" + char *smonth = mdate; + int day = 0; + int year = 0; + + // sscanf(mdate, "%s %d %d", bdt, &day, &year); // Not implemented in 2.3.0 and probably too much code + uint8_t i = 0; + for (char *str = strtok_r(mdate, " ", &p); str && i < 3; str = strtok_r(NULL, " ", &p)) + { + switch (i++) + { + case 0: // Month + smonth = str; + break; + case 1: // Day + day = atoi(str); + break; + case 2: // Year + year = atoi(str); + } + } + int month = (strstr(kMonthNamesEnglish, smonth) - kMonthNamesEnglish) / 3 + 1; + snprintf_P(bdt, sizeof(bdt), PSTR("%d-%02d-%02d %s"), year, month, day, __TIME__); + return String(bdt); // 2017-03-07T11:08:02 +} + +String Rtc::msToHumanString(uint32_t const msecs) +{ + uint32_t totalseconds = msecs / 1000; + if (totalseconds == 0) + { + return F("0T00:00:00"); + } + + // Note: millis() can only count up to 45 days, so uint8_t is safe. + uint8_t days = totalseconds / (60 * 60 * 24); + uint8_t hours = (totalseconds / (60 * 60)) % 24; + uint8_t minutes = (totalseconds / 60) % 60; + uint8_t seconds = totalseconds % 60; + + char dt[16]; + snprintf_P(dt, sizeof(dt), PSTR("%dT%02d:%02d:%02d"), days, hours, minutes, seconds); + return String(dt); +} + +String Rtc::timeSince(uint32_t const start) +{ + if (start == 0) + return F("Never"); + uint32_t diff = 0; + uint32_t now = millis(); + if (start < now) + diff = now - start; + else + diff = UINT32_MAX - start + now; + return msToHumanString(diff) + " ago"; +} + +void Rtc::breakTime(uint32_t time_input, TIME_T &tm) +{ + // break the given time_input into time components + // this is a more compact version of the C library localtime function + // note that year is offset from 1970 !!! + + uint8_t year; + uint8_t month; + uint8_t month_length; + uint32_t time; + unsigned long days; + + time = time_input; + tm.second = time % 60; + time /= 60; // now it is minutes + tm.minute = time % 60; + time /= 60; // now it is hours + tm.hour = time % 24; + time /= 24; // now it is days + tm.days = time; + tm.day_of_week = ((time + 4) % 7) + 1; // Sunday is day 1 + + year = 0; + days = 0; + while ((unsigned)(days += (LEAP_YEAR(year) ? 366 : 365)) <= time) + { + year++; + } + tm.year = year + 1970; // year is offset from 1970 + + days -= LEAP_YEAR(year) ? 366 : 365; + time -= days; // now it is days in this year, starting at 0 + tm.day_of_year = time; + + days = 0; + month = 0; + month_length = 0; + for (month = 0; month < 12; month++) + { + if (1 == month) + { // february + if (LEAP_YEAR(year)) + { + month_length = 29; + } + else + { + month_length = 28; + } + } + else + { + month_length = kDaysInMonth[month]; + } + + if (time >= month_length) + { + time -= month_length; + } + else + { + break; + } + } + tm.month = month + 1; // jan is month 1 + tm.day_of_month = time + 1; // day of month + tm.valid = (time_input > 1451602800); // 2016-01-01 +} + +void Rtc::loop() +{ + if (bitRead(operationFlag, 0)) + { + bitClear(operationFlag, 0); + getNtp(); + } +} + +void Rtc::getNtp() +{ + if (WiFi.status() == WL_CONNECTED) + { + uint32_t ntp_time = sntp_get_current_timestamp(); + if (ntp_time > 1451602800) + { + utcTime = ntp_time; + breakTime(utcTime, rtcTime); + Debug::AddInfo(PSTR("NTP: %04d-%02d-%02d %02d:%02d:%02d"), rtcTime.year, rtcTime.month, rtcTime.day_of_month, rtcTime.hour, rtcTime.minute, rtcTime.second); + } + } +} + +void Rtc::perSecondDo() +{ + bool isAdd = false; + if (utcTime == 0 || perSecond % 600 == 0) + { + bitSet(operationFlag, 0); + } + if (utcTime > 0) + { + utcTime += 1; + breakTime(utcTime, rtcTime); + //Debug::AddInfo(PSTR("Ticker: %04d-%02d-%02d %02d:%02d:%02d"), rtcTime.year, rtcTime.month, rtcTime.day_of_month, rtcTime.hour, rtcTime.minute, rtcTime.second); + } +} + +void Rtc::init() +{ + if (globalConfig.wifi.ntp[0] != '\0') + { + Debug::AddInfo(PSTR("NTP Server: %s"), globalConfig.wifi.ntp); + sntp_setservername(0, globalConfig.wifi.ntp); + } + else + { + Debug::AddInfo(PSTR("NTP Server: default")); + sntp_setservername(0, (char *)"120.25.115.20"); + sntp_setservername(1, (char *)"203.107.6.88"); + sntp_setservername(2, (char *)"ntp3.aliyun.com"); + } + sntp_stop(); + sntp_set_timezone(8); + sntp_init(); + utcTime = 0; +} + +uint32_t Rtc::getRtcRebootCrc() +{ + uint32_t crc = 0; + uint8_t *bytes = (uint8_t *)&rtcReboot; + for (uint32_t i = 0; i < sizeof(RtcReboot); i++) + { + crc += bytes[i] * (i + 1); + } + return crc; +} + +void Rtc::rtcRebootLoad() +{ + ESP.rtcUserMemoryRead(100 - sizeof(RtcReboot), (uint32_t *)&rtcReboot, sizeof(RtcReboot)); // 0x280 + if (rtcReboot.valid != RTC_MEM_VALID) + { + memset(&rtcReboot, 0, sizeof(RtcReboot)); + rtcReboot.valid = RTC_MEM_VALID; + rtcReboot.fast_reboot_count = 0; + rtcRebootSave(); + } + rtcRebootCrc = getRtcRebootCrc(); +} + +void Rtc::rtcRebootSave() +{ + if (getRtcRebootCrc() != rtcRebootCrc) + { + rtcReboot.valid = RTC_MEM_VALID; + ESP.rtcUserMemoryWrite(100 - sizeof(RtcReboot), (uint32_t *)&rtcReboot, sizeof(RtcReboot)); + rtcRebootCrc = getRtcRebootCrc(); + } +} diff --git a/lib/esp_framework/src/Util.cpp b/lib/esp_framework/src/Util.cpp new file mode 100644 index 0000000..d85a4a7 --- /dev/null +++ b/lib/esp_framework/src/Util.cpp @@ -0,0 +1,82 @@ +#include "Util.h" + +char *Util::strlowr(char *str) +{ + char *orign = str; + for (; *str != '\0'; str++) + *str = tolower(*str); + return orign; +} + +char *Util::strupr(char *str) +{ + char *orign = str; + for (; *str != '\0'; str++) + *str = toupper(*str); + return orign; +} + +uint16_t Util::hex2Str(uint8_t *bin, uint16_t bin_size, char *buff, bool needBlank) +{ + const char *set = "0123456789ABCDEF"; + char *nptr = buff; + if (NULL == buff) + { + return -1; + } + uint16_t len = needBlank ? (bin_size * 2 + bin_size) : (bin_size * 2 + 1); + while (bin_size--) + { + *nptr++ = set[(*bin) >> 4]; + *nptr++ = set[(*bin++) & 0xF]; + if (needBlank && bin_size > 0) + { + *nptr++ = ' '; + } + } + *nptr = '\0'; + return len; +} + +char *Util::dtostrfd(double number, unsigned char prec, char *s) +{ + if ((isnan(number)) || (isinf(number))) + { // Fix for JSON output (https://stackoverflow.com/questions/1423081/json-left-out-infinity-and-nan-json-status-in-ecmascript) + strcpy(s, "null"); + return s; + } + else + { + return dtostrf(number, 1, prec, s); + } +} + +uint32_t Util::SqrtInt(uint32_t num) +{ + if (num <= 1) + { + return num; + } + + uint32_t x = num / 2; + uint32_t y; + do + { + y = (x + num / x) / 2; + if (y >= x) + { + return x; + } + x = y; + } while (true); +} + +uint32_t Util::RoundSqrtInt(uint32_t num) +{ + uint32_t s = SqrtInt(4 * num); + if (s & 1) + { + s++; + } + return s / 2; +} \ No newline at end of file diff --git a/lib/esp_framework/src/Wifi.cpp b/lib/esp_framework/src/Wifi.cpp new file mode 100644 index 0000000..4a34c67 --- /dev/null +++ b/lib/esp_framework/src/Wifi.cpp @@ -0,0 +1,215 @@ +#include +#include +#include +#include +#include "Wifi.h" +#include "Debug.h" + +WiFiClient Wifi::wifiClient; +WiFiEventHandler Wifi::STAGotIP; +//WiFiEventHandler Wifi::STADisconnected; +bool Wifi::isDHCP = true; + +unsigned long Wifi::configPortalStart = 0; +//unsigned long Wifi::connectStart = 0; +bool Wifi::connect = false; +String Wifi::_ssid = ""; +String Wifi::_pass = ""; +DNSServer *Wifi::dnsServer; + +void Wifi::connectWifi() +{ + delay(50); + if (globalConfig.wifi.ssid[0] != '\0') + { + setupWifi(); + } + else + { + setupWifiManager(true); + } +} + +void Wifi::setupWifi() +{ + WiFi.persistent(false); // Solve possible wifi init errors (re-add at 6.2.1.16 #4044, #4083) + WiFi.disconnect(true); // Delete SDK wifi config + delay(200); + WiFi.mode(WIFI_STA); + WiFi.setAutoConnect(true); + WiFi.setAutoReconnect(true); + WiFi.hostname(UID); + Debug::AddInfo(PSTR("Connecting to %s %s Wifi"), globalConfig.wifi.ssid, globalConfig.wifi.pass); + STAGotIP = WiFi.onStationModeGotIP([](const WiFiEventStationModeGotIP &event) { + //connectStart = 0; + Debug::AddInfo(PSTR("WiFi1 connected. SSID: %s IP address: %s"), WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); + if (globalConfig.wifi.is_static && String(globalConfig.wifi.ip).equals(WiFi.localIP().toString())) + { + isDHCP = false; + } + /* + STADisconnected = WiFi.onStationModeDisconnected([](const WiFiEventStationModeDisconnected &event) { + if (connectStart == 0) + { + connectStart = millis() + ConnectTimeOut * 1000; + } + Debug::AddInfo(PSTR("onStationModeDisconnected")); + STADisconnected = NULL; + }); + */ + }); + if (globalConfig.wifi.is_static) + { + isDHCP = false; + IPAddress static_ip; + IPAddress static_sn; + IPAddress static_gw; + static_ip.fromString(globalConfig.wifi.ip); + static_sn.fromString(globalConfig.wifi.sn); + static_gw.fromString(globalConfig.wifi.gw); + Debug::AddInfo(PSTR("Custom STA IP/GW/Subnet: %s %s %s"), globalConfig.wifi.ip, globalConfig.wifi.sn, globalConfig.wifi.gw); + WiFi.config(static_ip, static_gw, static_sn); + } + + //connectStart = millis(); + WiFi.begin(globalConfig.wifi.ssid, globalConfig.wifi.pass); +} + +void Wifi::setupWifiManager(bool resetSettings) +{ + if (resetSettings) + { + Debug::AddInfo(PSTR("WifiManager ResetSettings")); + Config::resetConfig(); + + Debug::AddInfo(PSTR("settings invalidated")); + Debug::AddInfo(PSTR("THIS MAY CAUSE AP NOT TO START UP PROPERLY. YOU NEED TO COMMENT IT OUT AFTER ERASING THE DATA.")); + WiFi.disconnect(true); + } + //WiFi.setAutoConnect(true); + //WiFi.setAutoReconnect(true); + WiFi.hostname(UID); + STAGotIP = WiFi.onStationModeGotIP([](const WiFiEventStationModeGotIP &event) { + Debug::AddInfo(PSTR("WiFi2 connected. SSID: %s IP address: %s"), WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); + }); + + configPortalStart = millis(); + if (WiFi.isConnected()) + { + WiFi.mode(WIFI_AP_STA); + Debug::AddInfo(PSTR("SET AP STA Mode")); + } + else + { + WiFi.persistent(false); + WiFi.disconnect(); + WiFi.mode(WIFI_AP_STA); + WiFi.persistent(true); + Debug::AddInfo(PSTR("SET AP Mode")); + } + + connect = false; + WiFi.softAP(UID); + delay(500); + Debug::AddInfo(PSTR("AP IP address: %s"), WiFi.softAPIP().toString().c_str()); + + dnsServer = new DNSServer(); + dnsServer->setErrorReplyCode(DNSReplyCode::NoError); + dnsServer->start(53, "*", WiFi.softAPIP()); +} + +void Wifi::tryConnect(String ssid, String pass) +{ + _ssid = ssid; + _pass = pass; + connect = true; +} + +void Wifi::loop() +{ + /* + if (connectStart > 0 && millis() > connectStart + (ConnectTimeOut * 1000)) + { + connectStart = 0; + if (!WiFi.isConnected()) + { + setupWifiManager(false); + return; + } + } + */ + if (configPortalStart == 0) + { + return; + } + dnsServer->processNextRequest(); + + if (connect) + { + connect = false; + Debug::AddInfo(PSTR("Connecting to new AP")); + + WiFi.begin(_ssid.c_str(), _pass.c_str()); + Debug::AddInfo(PSTR("Waiting for connection result with time out")); + } + + if (_ssid.length() > 0 && WiFi.isConnected()) + { + strcpy(globalConfig.wifi.ssid, _ssid.c_str()); + strcpy(globalConfig.wifi.pass, _pass.c_str()); + Config::saveConfig(); + + // 为了使WEB获取到IP 2秒后才关闭AP + Ticker *ticker = new Ticker(); + ticker->attach(3, []() { + WiFi.mode(WIFI_STA); + Debug::AddInfo(PSTR("SET STA Mode")); + ESP.reset(); + }); + + Debug::AddInfo(PSTR("WiFi connected. SSID: %s IP address: %s"), WiFi.SSID().c_str(), WiFi.localIP().toString().c_str()); + + dnsServer->stop(); + configPortalStart = 0; + _ssid = ""; + _pass = ""; + return; + } + + // 检查是否超时 + if (millis() > configPortalStart + (ConfigPortalTimeOut * 1000)) + { + dnsServer->stop(); + configPortalStart = 0; + _ssid = ""; + _pass = ""; + Debug::AddInfo(PSTR("startConfigPortal TimeOut")); + if (WiFi.isConnected()) + { + // 为了使WEB获取到IP 2秒后才关闭AP + Ticker *ticker = new Ticker(); + ticker->attach(3, []() { + WiFi.mode(WIFI_STA); + Debug::AddInfo(PSTR("SET STA Mode")); + ESP.reset(); + }); + } + else + { + Debug::AddInfo(PSTR("Wifi failed to connect and hit timeout. Rebooting...")); + delay(3000); + ESP.reset(); // 重置,可能进入深度睡眠状态 + delay(5000); + } + } +} + +bool Wifi::isIp(String str) +{ + int a, b, c, d; + if ((sscanf(str.c_str(), "%d.%d.%d.%d", &a, &b, &c, &d) == 4) && (a >= 0 && a <= 255) && (b >= 0 && b <= 255) && (c >= 0 && c <= 255) && (d >= 0 && d <= 255)) + { + return true; + } + return false; +} \ No newline at end of file diff --git a/nanopb/RelayConfig.proto b/nanopb/RelayConfig.proto new file mode 100644 index 0000000..d9d2cbc --- /dev/null +++ b/nanopb/RelayConfig.proto @@ -0,0 +1,27 @@ + +syntax = "proto3"; + +import "nanopb.proto"; + +message RelayConfigMessage { + uint32 led_type = 1 [(nanopb).int_size = IS_8]; + uint32 led_start = 2 [(nanopb).int_size = IS_16]; + uint32 led_end = 3 [(nanopb).int_size = IS_16]; + uint32 power_on_state = 4 [(nanopb).int_size = IS_8]; + uint32 last_state = 5 [(nanopb).int_size = IS_8]; + repeated uint32 study_index = 6 [(nanopb).max_count = 4, (nanopb).fixed_count = true]; + repeated uint32 study = 7 [(nanopb).max_count = 40, (nanopb).fixed_count = true]; + uint32 led_light = 8 [(nanopb).int_size = IS_8]; + uint32 led_time = 9 [(nanopb).int_size = IS_8]; + + uint32 downlight_ch = 10 [(nanopb).int_size = IS_8]; + uint32 downlight_index = 11 [(nanopb).int_size = IS_8]; + repeated uint32 downlight_color = 12 [(nanopb).max_count = 3, (nanopb).fixed_count = true, (nanopb).int_size = IS_8]; + uint32 downlight_default = 13 [(nanopb).int_size = IS_8]; + uint32 downlight_interval = 14 [(nanopb).int_size = IS_16]; + + uint32 power_mode = 19 [(nanopb).int_size = IS_8]; + + uint32 module_type = 20 [(nanopb).int_size = IS_8]; + uint32 report_interval = 21 [(nanopb).int_size = IS_16]; +} diff --git a/nanopb/descriptor.proto b/nanopb/descriptor.proto new file mode 100644 index 0000000..8697a50 --- /dev/null +++ b/nanopb/descriptor.proto @@ -0,0 +1,872 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: kenton@google.com (Kenton Varda) +// Based on original Protocol Buffers design by +// Sanjay Ghemawat, Jeff Dean, and others. +// +// The messages in this file describe the definitions found in .proto files. +// A valid .proto file can be translated directly to a FileDescriptorProto +// without any other information (e.g. without reading its imports). + + +syntax = "proto2"; + +package google.protobuf; +option go_package = "github.com/golang/protobuf/protoc-gen-go/descriptor;descriptor"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "DescriptorProtos"; +option csharp_namespace = "Google.Protobuf.Reflection"; +option objc_class_prefix = "GPB"; +option cc_enable_arenas = true; + +// descriptor.proto must be optimized for speed because reflection-based +// algorithms don't work during bootstrapping. +option optimize_for = SPEED; + +// The protocol compiler can output a FileDescriptorSet containing the .proto +// files it parses. +message FileDescriptorSet { + repeated FileDescriptorProto file = 1; +} + +// Describes a complete .proto file. +message FileDescriptorProto { + optional string name = 1; // file name, relative to root of source tree + optional string package = 2; // e.g. "foo", "foo.bar", etc. + + // Names of files imported by this file. + repeated string dependency = 3; + // Indexes of the public imported files in the dependency list above. + repeated int32 public_dependency = 10; + // Indexes of the weak imported files in the dependency list. + // For Google-internal migration only. Do not use. + repeated int32 weak_dependency = 11; + + // All top-level definitions in this file. + repeated DescriptorProto message_type = 4; + repeated EnumDescriptorProto enum_type = 5; + repeated ServiceDescriptorProto service = 6; + repeated FieldDescriptorProto extension = 7; + + optional FileOptions options = 8; + + // This field contains optional information about the original source code. + // You may safely remove this entire field without harming runtime + // functionality of the descriptors -- the information is needed only by + // development tools. + optional SourceCodeInfo source_code_info = 9; + + // The syntax of the proto file. + // The supported values are "proto2" and "proto3". + optional string syntax = 12; +} + +// Describes a message type. +message DescriptorProto { + optional string name = 1; + + repeated FieldDescriptorProto field = 2; + repeated FieldDescriptorProto extension = 6; + + repeated DescriptorProto nested_type = 3; + repeated EnumDescriptorProto enum_type = 4; + + message ExtensionRange { + optional int32 start = 1; + optional int32 end = 2; + + optional ExtensionRangeOptions options = 3; + } + repeated ExtensionRange extension_range = 5; + + repeated OneofDescriptorProto oneof_decl = 8; + + optional MessageOptions options = 7; + + // Range of reserved tag numbers. Reserved tag numbers may not be used by + // fields or extension ranges in the same message. Reserved ranges may + // not overlap. + message ReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Exclusive. + } + repeated ReservedRange reserved_range = 9; + // Reserved field names, which may not be used by fields in the same message. + // A given name may only be reserved once. + repeated string reserved_name = 10; +} + +message ExtensionRangeOptions { + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +// Describes a field within a message. +message FieldDescriptorProto { + enum Type { + // 0 is reserved for errors. + // Order is weird for historical reasons. + TYPE_DOUBLE = 1; + TYPE_FLOAT = 2; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if + // negative values are likely. + TYPE_INT64 = 3; + TYPE_UINT64 = 4; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if + // negative values are likely. + TYPE_INT32 = 5; + TYPE_FIXED64 = 6; + TYPE_FIXED32 = 7; + TYPE_BOOL = 8; + TYPE_STRING = 9; + // Tag-delimited aggregate. + // Group type is deprecated and not supported in proto3. However, Proto3 + // implementations should still be able to parse the group wire format and + // treat group fields as unknown fields. + TYPE_GROUP = 10; + TYPE_MESSAGE = 11; // Length-delimited aggregate. + + // New in version 2. + TYPE_BYTES = 12; + TYPE_UINT32 = 13; + TYPE_ENUM = 14; + TYPE_SFIXED32 = 15; + TYPE_SFIXED64 = 16; + TYPE_SINT32 = 17; // Uses ZigZag encoding. + TYPE_SINT64 = 18; // Uses ZigZag encoding. + }; + + enum Label { + // 0 is reserved for errors + LABEL_OPTIONAL = 1; + LABEL_REQUIRED = 2; + LABEL_REPEATED = 3; + }; + + optional string name = 1; + optional int32 number = 3; + optional Label label = 4; + + // If type_name is set, this need not be set. If both this and type_name + // are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. + optional Type type = 5; + + // For message and enum types, this is the name of the type. If the name + // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping + // rules are used to find the type (i.e. first the nested types within this + // message are searched, then within the parent, on up to the root + // namespace). + optional string type_name = 6; + + // For extensions, this is the name of the type being extended. It is + // resolved in the same manner as type_name. + optional string extendee = 2; + + // For numeric types, contains the original text representation of the value. + // For booleans, "true" or "false". + // For strings, contains the default text contents (not escaped in any way). + // For bytes, contains the C escaped value. All bytes >= 128 are escaped. + // TODO(kenton): Base-64 encode? + optional string default_value = 7; + + // If set, gives the index of a oneof in the containing type's oneof_decl + // list. This field is a member of that oneof. + optional int32 oneof_index = 9; + + // JSON name of this field. The value is set by protocol compiler. If the + // user has set a "json_name" option on this field, that option's value + // will be used. Otherwise, it's deduced from the field's name by converting + // it to camelCase. + optional string json_name = 10; + + optional FieldOptions options = 8; +} + +// Describes a oneof. +message OneofDescriptorProto { + optional string name = 1; + optional OneofOptions options = 2; +} + +// Describes an enum type. +message EnumDescriptorProto { + optional string name = 1; + + repeated EnumValueDescriptorProto value = 2; + + optional EnumOptions options = 3; + + // Range of reserved numeric values. Reserved values may not be used by + // entries in the same enum. Reserved ranges may not overlap. + // + // Note that this is distinct from DescriptorProto.ReservedRange in that it + // is inclusive such that it can appropriately represent the entire int32 + // domain. + message EnumReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Inclusive. + } + + // Range of reserved numeric values. Reserved numeric values may not be used + // by enum values in the same enum declaration. Reserved ranges may not + // overlap. + repeated EnumReservedRange reserved_range = 4; + + // Reserved enum value names, which may not be reused. A given name may only + // be reserved once. + repeated string reserved_name = 5; +} + +// Describes a value within an enum. +message EnumValueDescriptorProto { + optional string name = 1; + optional int32 number = 2; + + optional EnumValueOptions options = 3; +} + +// Describes a service. +message ServiceDescriptorProto { + optional string name = 1; + repeated MethodDescriptorProto method = 2; + + optional ServiceOptions options = 3; +} + +// Describes a method of a service. +message MethodDescriptorProto { + optional string name = 1; + + // Input and output type names. These are resolved in the same way as + // FieldDescriptorProto.type_name, but must refer to a message type. + optional string input_type = 2; + optional string output_type = 3; + + optional MethodOptions options = 4; + + // Identifies if client streams multiple client messages + optional bool client_streaming = 5 [default=false]; + // Identifies if server streams multiple server messages + optional bool server_streaming = 6 [default=false]; +} + + +// =================================================================== +// Options + +// Each of the definitions above may have "options" attached. These are +// just annotations which may cause code to be generated slightly differently +// or may contain hints for code that manipulates protocol messages. +// +// Clients may define custom options as extensions of the *Options messages. +// These extensions may not yet be known at parsing time, so the parser cannot +// store the values in them. Instead it stores them in a field in the *Options +// message called uninterpreted_option. This field must have the same name +// across all *Options messages. We then use this field to populate the +// extensions when we build a descriptor, at which point all protos have been +// parsed and so all extensions are known. +// +// Extension numbers for custom options may be chosen as follows: +// * For options which will only be used within a single application or +// organization, or for experimental options, use field numbers 50000 +// through 99999. It is up to you to ensure that you do not use the +// same number for multiple options. +// * For options which will be published and used publicly by multiple +// independent entities, e-mail protobuf-global-extension-registry@google.com +// to reserve extension numbers. Simply provide your project name (e.g. +// Objective-C plugin) and your project website (if available) -- there's no +// need to explain how you intend to use them. Usually you only need one +// extension number. You can declare multiple options with only one extension +// number by putting them in a sub-message. See the Custom Options section of +// the docs for examples: +// https://developers.google.com/protocol-buffers/docs/proto#options +// If this turns out to be popular, a web service will be set up +// to automatically assign option numbers. + + +message FileOptions { + + // Sets the Java package where classes generated from this .proto will be + // placed. By default, the proto package is used, but this is often + // inappropriate because proto packages do not normally start with backwards + // domain names. + optional string java_package = 1; + + + // If set, all the classes from the .proto file are wrapped in a single + // outer class with the given name. This applies to both Proto1 + // (equivalent to the old "--one_java_file" option) and Proto2 (where + // a .proto always translates to a single class, but you may want to + // explicitly choose the class name). + optional string java_outer_classname = 8; + + // If set true, then the Java code generator will generate a separate .java + // file for each top-level message, enum, and service defined in the .proto + // file. Thus, these types will *not* be nested inside the outer class + // named by java_outer_classname. However, the outer class will still be + // generated to contain the file's getDescriptor() method as well as any + // top-level extensions defined in the file. + optional bool java_multiple_files = 10 [default=false]; + + // This option does nothing. + optional bool java_generate_equals_and_hash = 20 [deprecated=true]; + + // If set true, then the Java2 code generator will generate code that + // throws an exception whenever an attempt is made to assign a non-UTF-8 + // byte sequence to a string field. + // Message reflection will do the same. + // However, an extension field still accepts non-UTF-8 byte sequences. + // This option has no effect on when used with the lite runtime. + optional bool java_string_check_utf8 = 27 [default=false]; + + + // Generated classes can be optimized for speed or code size. + enum OptimizeMode { + SPEED = 1; // Generate complete code for parsing, serialization, + // etc. + CODE_SIZE = 2; // Use ReflectionOps to implement these methods. + LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime. + } + optional OptimizeMode optimize_for = 9 [default=SPEED]; + + // Sets the Go package where structs generated from this .proto will be + // placed. If omitted, the Go package will be derived from the following: + // - The basename of the package import path, if provided. + // - Otherwise, the package statement in the .proto file, if present. + // - Otherwise, the basename of the .proto file, without extension. + optional string go_package = 11; + + + + // Should generic services be generated in each language? "Generic" services + // are not specific to any particular RPC system. They are generated by the + // main code generators in each language (without additional plugins). + // Generic services were the only kind of service generation supported by + // early versions of google.protobuf. + // + // Generic services are now considered deprecated in favor of using plugins + // that generate code specific to your particular RPC system. Therefore, + // these default to false. Old code which depends on generic services should + // explicitly set them to true. + optional bool cc_generic_services = 16 [default=false]; + optional bool java_generic_services = 17 [default=false]; + optional bool py_generic_services = 18 [default=false]; + optional bool php_generic_services = 42 [default=false]; + + // Is this file deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for everything in the file, or it will be completely ignored; in the very + // least, this is a formalization for deprecating files. + optional bool deprecated = 23 [default=false]; + + // Enables the use of arenas for the proto messages in this file. This applies + // only to generated classes for C++. + optional bool cc_enable_arenas = 31 [default=false]; + + + // Sets the objective c class prefix which is prepended to all objective c + // generated classes from this .proto. There is no default. + optional string objc_class_prefix = 36; + + // Namespace for generated classes; defaults to the package. + optional string csharp_namespace = 37; + + // By default Swift generators will take the proto package and CamelCase it + // replacing '.' with underscore and use that to prefix the types/symbols + // defined. When this options is provided, they will use this value instead + // to prefix the types/symbols defined. + optional string swift_prefix = 39; + + // Sets the php class prefix which is prepended to all php generated classes + // from this .proto. Default is empty. + optional string php_class_prefix = 40; + + // Use this option to change the namespace of php generated classes. Default + // is empty. When this option is empty, the package name will be used for + // determining the namespace. + optional string php_namespace = 41; + + // The parser stores options it doesn't recognize here. + // See the documentation for the "Options" section above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. + // See the documentation for the "Options" section above. + extensions 1000 to max; + + reserved 38; +} + +message MessageOptions { + // Set true to use the old proto1 MessageSet wire format for extensions. + // This is provided for backwards-compatibility with the MessageSet wire + // format. You should not use this for any other reason: It's less + // efficient, has fewer features, and is more complicated. + // + // The message must be defined exactly as follows: + // message Foo { + // option message_set_wire_format = true; + // extensions 4 to max; + // } + // Note that the message cannot have any defined fields; MessageSets only + // have extensions. + // + // All extensions of your type must be singular messages; e.g. they cannot + // be int32s, enums, or repeated messages. + // + // Because this is an option, the above two restrictions are not enforced by + // the protocol compiler. + optional bool message_set_wire_format = 1 [default=false]; + + // Disables the generation of the standard "descriptor()" accessor, which can + // conflict with a field of the same name. This is meant to make migration + // from proto1 easier; new code should avoid fields named "descriptor". + optional bool no_standard_descriptor_accessor = 2 [default=false]; + + // Is this message deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the message, or it will be completely ignored; in the very least, + // this is a formalization for deprecating messages. + optional bool deprecated = 3 [default=false]; + + // Whether the message is an automatically generated map entry type for the + // maps field. + // + // For maps fields: + // map map_field = 1; + // The parsed descriptor looks like: + // message MapFieldEntry { + // option map_entry = true; + // optional KeyType key = 1; + // optional ValueType value = 2; + // } + // repeated MapFieldEntry map_field = 1; + // + // Implementations may choose not to generate the map_entry=true message, but + // use a native map in the target language to hold the keys and values. + // The reflection APIs in such implementions still need to work as + // if the field is a repeated message field. + // + // NOTE: Do not set the option in .proto files. Always use the maps syntax + // instead. The option should only be implicitly set by the proto compiler + // parser. + optional bool map_entry = 7; + + reserved 8; // javalite_serializable + reserved 9; // javanano_as_lite + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message FieldOptions { + // The ctype option instructs the C++ code generator to use a different + // representation of the field than it normally would. See the specific + // options below. This option is not yet implemented in the open source + // release -- sorry, we'll try to include it in a future version! + optional CType ctype = 1 [default = STRING]; + enum CType { + // Default mode. + STRING = 0; + + CORD = 1; + + STRING_PIECE = 2; + } + // The packed option can be enabled for repeated primitive fields to enable + // a more efficient representation on the wire. Rather than repeatedly + // writing the tag and type for each element, the entire array is encoded as + // a single length-delimited blob. In proto3, only explicit setting it to + // false will avoid using packed encoding. + optional bool packed = 2; + + // The jstype option determines the JavaScript type used for values of the + // field. The option is permitted only for 64 bit integral and fixed types + // (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING + // is represented as JavaScript string, which avoids loss of precision that + // can happen when a large value is converted to a floating point JavaScript. + // Specifying JS_NUMBER for the jstype causes the generated JavaScript code to + // use the JavaScript "number" type. The behavior of the default option + // JS_NORMAL is implementation dependent. + // + // This option is an enum to permit additional types to be added, e.g. + // goog.math.Integer. + optional JSType jstype = 6 [default = JS_NORMAL]; + enum JSType { + // Use the default type. + JS_NORMAL = 0; + + // Use JavaScript strings. + JS_STRING = 1; + + // Use JavaScript numbers. + JS_NUMBER = 2; + } + + // Should this field be parsed lazily? Lazy applies only to message-type + // fields. It means that when the outer message is initially parsed, the + // inner message's contents will not be parsed but instead stored in encoded + // form. The inner message will actually be parsed when it is first accessed. + // + // This is only a hint. Implementations are free to choose whether to use + // eager or lazy parsing regardless of the value of this option. However, + // setting this option true suggests that the protocol author believes that + // using lazy parsing on this field is worth the additional bookkeeping + // overhead typically needed to implement it. + // + // This option does not affect the public interface of any generated code; + // all method signatures remain the same. Furthermore, thread-safety of the + // interface is not affected by this option; const methods remain safe to + // call from multiple threads concurrently, while non-const methods continue + // to require exclusive access. + // + // + // Note that implementations may choose not to check required fields within + // a lazy sub-message. That is, calling IsInitialized() on the outer message + // may return true even if the inner message has missing required fields. + // This is necessary because otherwise the inner message would have to be + // parsed in order to perform the check, defeating the purpose of lazy + // parsing. An implementation which chooses not to check required fields + // must be consistent about it. That is, for any particular sub-message, the + // implementation must either *always* check its required fields, or *never* + // check its required fields, regardless of whether or not the message has + // been parsed. + optional bool lazy = 5 [default=false]; + + // Is this field deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for accessors, or it will be completely ignored; in the very least, this + // is a formalization for deprecating fields. + optional bool deprecated = 3 [default=false]; + + // For Google-internal migration only. Do not use. + optional bool weak = 10 [default=false]; + + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; + + reserved 4; // removed jtype +} + +message OneofOptions { + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumOptions { + + // Set this option to true to allow mapping different tag names to the same + // value. + optional bool allow_alias = 2; + + // Is this enum deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum, or it will be completely ignored; in the very least, this + // is a formalization for deprecating enums. + optional bool deprecated = 3 [default=false]; + + reserved 5; // javanano_as_lite + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumValueOptions { + // Is this enum value deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum value, or it will be completely ignored; in the very least, + // this is a formalization for deprecating enum values. + optional bool deprecated = 1 [default=false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message ServiceOptions { + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this service deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the service, or it will be completely ignored; in the very least, + // this is a formalization for deprecating services. + optional bool deprecated = 33 [default=false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message MethodOptions { + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this method deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the method, or it will be completely ignored; in the very least, + // this is a formalization for deprecating methods. + optional bool deprecated = 33 [default=false]; + + // Is this method side-effect-free (or safe in HTTP parlance), or idempotent, + // or neither? HTTP based RPC implementation may choose GET verb for safe + // methods, and PUT verb for idempotent methods instead of the default POST. + enum IdempotencyLevel { + IDEMPOTENCY_UNKNOWN = 0; + NO_SIDE_EFFECTS = 1; // implies idempotent + IDEMPOTENT = 2; // idempotent, but may have side effects + } + optional IdempotencyLevel idempotency_level = + 34 [default=IDEMPOTENCY_UNKNOWN]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + + +// A message representing a option the parser does not recognize. This only +// appears in options protos created by the compiler::Parser class. +// DescriptorPool resolves these when building Descriptor objects. Therefore, +// options protos in descriptor objects (e.g. returned by Descriptor::options(), +// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions +// in them. +message UninterpretedOption { + // The name of the uninterpreted option. Each string represents a segment in + // a dot-separated name. is_extension is true iff a segment represents an + // extension (denoted with parentheses in options specs in .proto files). + // E.g.,{ ["foo", false], ["bar.baz", true], ["qux", false] } represents + // "foo.(bar.baz).qux". + message NamePart { + required string name_part = 1; + required bool is_extension = 2; + } + repeated NamePart name = 2; + + // The value of the uninterpreted option, in whatever type the tokenizer + // identified it as during parsing. Exactly one of these should be set. + optional string identifier_value = 3; + optional uint64 positive_int_value = 4; + optional int64 negative_int_value = 5; + optional double double_value = 6; + optional bytes string_value = 7; + optional string aggregate_value = 8; +} + +// =================================================================== +// Optional source code info + +// Encapsulates information about the original source file from which a +// FileDescriptorProto was generated. +message SourceCodeInfo { + // A Location identifies a piece of source code in a .proto file which + // corresponds to a particular definition. This information is intended + // to be useful to IDEs, code indexers, documentation generators, and similar + // tools. + // + // For example, say we have a file like: + // message Foo { + // optional string foo = 1; + // } + // Let's look at just the field definition: + // optional string foo = 1; + // ^ ^^ ^^ ^ ^^^ + // a bc de f ghi + // We have the following locations: + // span path represents + // [a,i) [ 4, 0, 2, 0 ] The whole field definition. + // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). + // [c,d) [ 4, 0, 2, 0, 5 ] The type (string). + // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). + // [g,h) [ 4, 0, 2, 0, 3 ] The number (1). + // + // Notes: + // - A location may refer to a repeated field itself (i.e. not to any + // particular index within it). This is used whenever a set of elements are + // logically enclosed in a single code segment. For example, an entire + // extend block (possibly containing multiple extension definitions) will + // have an outer location whose path refers to the "extensions" repeated + // field without an index. + // - Multiple locations may have the same path. This happens when a single + // logical declaration is spread out across multiple places. The most + // obvious example is the "extend" block again -- there may be multiple + // extend blocks in the same scope, each of which will have the same path. + // - A location's span is not always a subset of its parent's span. For + // example, the "extendee" of an extension declaration appears at the + // beginning of the "extend" block and is shared by all extensions within + // the block. + // - Just because a location's span is a subset of some other location's span + // does not mean that it is a descendent. For example, a "group" defines + // both a type and a field in a single declaration. Thus, the locations + // corresponding to the type and field and their components will overlap. + // - Code which tries to interpret locations should probably be designed to + // ignore those that it doesn't understand, as more types of locations could + // be recorded in the future. + repeated Location location = 1; + message Location { + // Identifies which part of the FileDescriptorProto was defined at this + // location. + // + // Each element is a field number or an index. They form a path from + // the root FileDescriptorProto to the place where the definition. For + // example, this path: + // [ 4, 3, 2, 7, 1 ] + // refers to: + // file.message_type(3) // 4, 3 + // .field(7) // 2, 7 + // .name() // 1 + // This is because FileDescriptorProto.message_type has field number 4: + // repeated DescriptorProto message_type = 4; + // and DescriptorProto.field has field number 2: + // repeated FieldDescriptorProto field = 2; + // and FieldDescriptorProto.name has field number 1: + // optional string name = 1; + // + // Thus, the above path gives the location of a field name. If we removed + // the last element: + // [ 4, 3, 2, 7 ] + // this path refers to the whole field declaration (from the beginning + // of the label to the terminating semicolon). + repeated int32 path = 1 [packed=true]; + + // Always has exactly three or four elements: start line, start column, + // end line (optional, otherwise assumed same as start line), end column. + // These are packed into a single field for efficiency. Note that line + // and column numbers are zero-based -- typically you will want to add + // 1 to each before displaying to a user. + repeated int32 span = 2 [packed=true]; + + // If this SourceCodeInfo represents a complete declaration, these are any + // comments appearing before and after the declaration which appear to be + // attached to the declaration. + // + // A series of line comments appearing on consecutive lines, with no other + // tokens appearing on those lines, will be treated as a single comment. + // + // leading_detached_comments will keep paragraphs of comments that appear + // before (but not connected to) the current element. Each paragraph, + // separated by empty lines, will be one comment element in the repeated + // field. + // + // Only the comment content is provided; comment markers (e.g. //) are + // stripped out. For block comments, leading whitespace and an asterisk + // will be stripped from the beginning of each line other than the first. + // Newlines are included in the output. + // + // Examples: + // + // optional int32 foo = 1; // Comment attached to foo. + // // Comment attached to bar. + // optional int32 bar = 2; + // + // optional string baz = 3; + // // Comment attached to baz. + // // Another line attached to baz. + // + // // Comment attached to qux. + // // + // // Another line attached to qux. + // optional double qux = 4; + // + // // Detached comment for corge. This is not leading or trailing comments + // // to qux or corge because there are blank lines separating it from + // // both. + // + // // Detached comment for corge paragraph 2. + // + // optional string corge = 5; + // /* Block comment attached + // * to corge. Leading asterisks + // * will be removed. */ + // /* Block comment attached to + // * grault. */ + // optional int32 grault = 6; + // + // // ignored detached comments. + optional string leading_comments = 3; + optional string trailing_comments = 4; + repeated string leading_detached_comments = 6; + } +} + +// Describes the relationship between generated code and its original source +// file. A GeneratedCodeInfo message is associated with only one generated +// source file, but may contain references to different source .proto files. +message GeneratedCodeInfo { + // An Annotation connects some span of text in generated code to an element + // of its generating .proto file. + repeated Annotation annotation = 1; + message Annotation { + // Identifies the element in the original source .proto file. This field + // is formatted the same as SourceCodeInfo.Location.path. + repeated int32 path = 1 [packed=true]; + + // Identifies the filesystem path to the original source .proto. + optional string source_file = 2; + + // Identifies the starting offset in bytes in the generated code + // that relates to the identified object. + optional int32 begin = 3; + + // Identifies the ending offset in bytes in the generated code that + // relates to the identified offset. The end offset should be one past + // the last relevant byte (so the length of the text = end - begin). + optional int32 end = 4; + } +} diff --git a/nanopb/generator.bat b/nanopb/generator.bat new file mode 100644 index 0000000..df14252 --- /dev/null +++ b/nanopb/generator.bat @@ -0,0 +1,7 @@ +cd /d %~dp0 + +D:\Workspaces_Smarthome\esp\nanopb\generator-bin\protoc.exe --nanopb_out=. RelayConfig.proto +copy RelayConfig.pb.h ..\include /y +copy RelayConfig.pb.c ..\src /y +del RelayConfig.pb.h +del RelayConfig.pb.c diff --git a/nanopb/nanopb.proto b/nanopb/nanopb.proto new file mode 100644 index 0000000..e89eea1 --- /dev/null +++ b/nanopb/nanopb.proto @@ -0,0 +1,124 @@ +// Custom options for defining: +// - Maximum size of string/bytes +// - Maximum number of elements in array +// +// These are used by nanopb to generate statically allocable structures +// for memory-limited environments. + +syntax = "proto2"; +import "descriptor.proto"; + +option java_package = "fi.kapsi.koti.jpa.nanopb"; + +enum FieldType { + FT_DEFAULT = 0; // Automatically decide field type, generate static field if possible. + FT_CALLBACK = 1; // Always generate a callback field. + FT_POINTER = 4; // Always generate a dynamically allocated field. + FT_STATIC = 2; // Generate a static field or raise an exception if not possible. + FT_IGNORE = 3; // Ignore the field completely. + FT_INLINE = 5; // Legacy option, use the separate 'fixed_length' option instead +} + +enum IntSize { + IS_DEFAULT = 0; // Default, 32/64bit based on type in .proto + IS_8 = 8; + IS_16 = 16; + IS_32 = 32; + IS_64 = 64; +} + +enum TypenameMangling { + M_NONE = 0; // Default, no typename mangling + M_STRIP_PACKAGE = 1; // Strip current package name + M_FLATTEN = 2; // Only use last path component +} + +// This is the inner options message, which basically defines options for +// a field. When it is used in message or file scope, it applies to all +// fields. +message NanoPBOptions { + // Allocated size for 'bytes' and 'string' fields. + // For string fields, this should include the space for null terminator. + optional int32 max_size = 1; + + // Maximum length for 'string' fields. Setting this is equivalent + // to setting max_size to a value of length+1. + optional int32 max_length = 14; + + // Allocated number of entries in arrays ('repeated' fields) + optional int32 max_count = 2; + + // Size of integer fields. Can save some memory if you don't need + // full 32 bits for the value. + optional IntSize int_size = 7 [default = IS_DEFAULT]; + + // Force type of field (callback or static allocation) + optional FieldType type = 3 [default = FT_DEFAULT]; + + // Use long names for enums, i.e. EnumName_EnumValue. + optional bool long_names = 4 [default = true]; + + // Add 'packed' attribute to generated structs. + // Note: this cannot be used on CPUs that break on unaligned + // accesses to variables. + optional bool packed_struct = 5 [default = false]; + + // Add 'packed' attribute to generated enums. + optional bool packed_enum = 10 [default = false]; + + // Skip this message + optional bool skip_message = 6 [default = false]; + + // Generate oneof fields as normal optional fields instead of union. + optional bool no_unions = 8 [default = false]; + + // integer type tag for a message + optional uint32 msgid = 9; + + // decode oneof as anonymous union + optional bool anonymous_oneof = 11 [default = false]; + + // Proto3 singular field does not generate a "has_" flag + optional bool proto3 = 12 [default = false]; + + // Generate an enum->string mapping function (can take up lots of space). + optional bool enum_to_string = 13 [default = false]; + + // Generate bytes arrays with fixed length + optional bool fixed_length = 15 [default = false]; + + // Generate repeated field with fixed count + optional bool fixed_count = 16 [default = false]; + + // Mangle long names + optional TypenameMangling mangle_names = 17 [default = M_NONE]; +} + +// Extensions to protoc 'Descriptor' type in order to define options +// inside a .proto file. +// +// Protocol Buffers extension number registry +// -------------------------------- +// Project: Nanopb +// Contact: Petteri Aimonen +// Web site: http://kapsi.fi/~jpa/nanopb +// Extensions: 1010 (all types) +// -------------------------------- + +extend google.protobuf.FileOptions { + optional NanoPBOptions nanopb_fileopt = 1010; +} + +extend google.protobuf.MessageOptions { + optional NanoPBOptions nanopb_msgopt = 1010; +} + +extend google.protobuf.EnumOptions { + optional NanoPBOptions nanopb_enumopt = 1010; +} + +extend google.protobuf.FieldOptions { + optional NanoPBOptions nanopb = 1010; +} + + diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..1b1156c --- /dev/null +++ b/platformio.ini @@ -0,0 +1,73 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter, extra scripting +; Upload options: custom port, speed and extra flags +; Library options: dependencies, extra library storages +; +; Please visit documentation for the other options and examples +; http://docs.platformio.org/en/stable/projectconf.html + +[platformio] +;default_envs = relay-all + +[env] +framework = arduino +board = esp01_1m +board_build.f_cpu = 80000000L +board_build.flash_mode = dout +board_build.ldscript = eagle.flash.1m.ld + +; *** Esp8266 core for Arduino version 2.6.1 +platform = espressif8266@2.3.3 +build_flags = -D NDEBUG + -mtarget-align + -Wl,-Map,firmware.map +; -Wl,-Teagle.flash.1m.ld + -DBEARSSL_SSL_BASIC +; NONOSDK22x_190703 = 2.2.2-dev(38a443e) + -DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK22x_190703 +; lwIP 2 - Higher Bandwidth no Features + -DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH +; VTABLES in Flash + -DVTABLES_IN_FLASH +; No exception code in firmware + -fno-exceptions + -lstdc++ + + -D MQTT_MAX_PACKET_SIZE=768 + -D MQTT_SOCKET_TIMEOUT=5 + -D PB_FIELD_16BIT=1 + +; *** Fix espressif8266@1.7.0 induced undesired all warnings +build_unflags = -Wall + +monitor_speed = 115200 +upload_speed = 460800 +; *** Upload Serial reset method for Wemos and NodeMCU +upload_resetmethod = nodemcu +;upload_port = COM5 +extra_scripts = scripts/strip-floats.py + scripts/name-firmware.py + +lib_deps = + PubSubClient + Nanopb=https://github.com/nanopb/nanopb.git#nanopb-0.3.9.5 + +[env:relay-all] +board_build.variant = esp8285 +build_flags = ${env.build_flags} -D USE_RCSWITCH -D USE_TRICOLOR +lib_deps = ${env.lib_deps} + rc-switch + +[env:relay-433] +board_build.variant = esp8285 +build_flags = ${env.build_flags} -D USE_RCSWITCH +lib_deps = ${env.lib_deps} + rc-switch + +[env:relay-tricolor] +board_build.variant = esp8285 +build_flags = ${env.build_flags} -D USE_TRICOLOR + +[env:relay-mini] +board_build.variant = esp8285 \ No newline at end of file diff --git a/scripts/name-firmware.py b/scripts/name-firmware.py new file mode 100644 index 0000000..fc39df1 --- /dev/null +++ b/scripts/name-firmware.py @@ -0,0 +1,33 @@ +Import('env') +import os +import shutil + +OUTPUT_DIR = "output{}".format(os.path.sep) + +def bin_map_copy(source, target, env): + variant = str(target[0]).split(os.path.sep)[2] + #print(variant) + #print(str(target[0])) + + o = str(target[0]).replace("firmware.bin", "") + dirs = os.listdir(o) + for file in dirs: + p = os.path.join(o, file, "esp_framework\\Rtc.cpp.o") + if os.path.isfile(p): + os.remove(p) + + # check if output directories exist and create if necessary + if not os.path.isdir(OUTPUT_DIR): + os.mkdir(OUTPUT_DIR) + + # create string with location and file names based on variant + bin_file = "{}{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant) + + if os.path.isfile(bin_file): + os.remove(bin_file) + + # copy firmware.bin to firmware/.bin + shutil.copy(str(target[0]), bin_file) + + +env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_map_copy]) diff --git a/scripts/strip-floats.py b/scripts/strip-floats.py new file mode 100644 index 0000000..da916eb --- /dev/null +++ b/scripts/strip-floats.py @@ -0,0 +1,15 @@ +Import('env') + +# +# Dump build environment (for debug) +#print env.Dump() +# + +flags = " ".join(env['LINKFLAGS']) +flags = flags.replace("-u _printf_float", "") +flags = flags.replace("-u _scanf_float", "") +newflags = flags.split() + +env.Replace( + LINKFLAGS=newflags +) \ No newline at end of file diff --git a/src/RadioReceive.cpp b/src/RadioReceive.cpp new file mode 100644 index 0000000..6e8f9c7 --- /dev/null +++ b/src/RadioReceive.cpp @@ -0,0 +1,153 @@ +#ifdef USE_RCSWITCH +#include "RadioReceive.h" +#include "Relay.h" +#include "RCSwitch.h" + +void RadioReceive::init(Relay *_relay, uint8_t io) +{ + relay = _relay; + mySwitch = new RCSwitch(); + pinMode(io, INPUT); + mySwitch->enableReceive(digitalPinToInterrupt(io)); +} + +void RadioReceive::study(uint8_t ch) +{ + studyCH = 10 + ch; + studyTime = millis(); + Debug::AddInfo(PSTR("Receive study . . . ")); +} + +void RadioReceive::del(uint8_t ch) +{ + studyCH = 20 + ch; + studyTime = millis(); + Debug::AddInfo(PSTR("Receive del . . . ")); +} + +void RadioReceive::delAll() +{ + relay->config.study_index[0] = 0; + relay->config.study_index[1] = 0; + relay->config.study_index[2] = 0; + relay->config.study_index[3] = 0; + Config::saveConfig(); + Debug::AddInfo(PSTR("Receive delAll . . . ")); +} + +void RadioReceive::loop() +{ + if (studyCH != 0 && millis() > studyTime + 10000) // 10秒超时 + { + Debug::AddInfo(PSTR("Receive study timeout")); + studyCH = 0; + } + + if (!mySwitch->available()) + { + return; + } + + unsigned long value = mySwitch->getReceivedValue(); + //Debug::AddError(PSTR("315Mhz: %d"), value); + mySwitch->resetAvailable(); + if (lastVaue == value && millis() < lastTime + 300) + { + return; + } + lastVaue = value; + lastTime = millis(); + + if (studyCH == 0) + { + bool isOk = false; + + /* + for (size_t i = 0; i < 40; i++) + { + if (config.relay_study[i] != 0) + { + Debug::AddInfo(PSTR("study %d %d"), i, config.relay_study[i]); + } + } + */ + + for (size_t ch = 0; ch < relay->channels; ch++) + { + //Debug::AddInfo(PSTR("study channel %d index %d"), ch, config.relay_study_index[ch]); + for (size_t i = 0; i < relay->config.study_index[ch]; i++) + { + //Debug::AddInfo(PSTR("study id %d"), (ch * 10) + i); + if (relay->config.study[(ch * 10) + i] == value) + { + isOk = true; + Debug::AddInfo(PSTR("Received %d to channel %d"), value, ch + 1); + relay->switchRelay(ch, !bitRead(relay->lastState, ch), true); + break; + } + } + } + if (!isOk) + { + Debug::AddInfo(PSTR("Receive %d no channel"), value); + } + Led::led(200); + } + else if (studyCH >= 20) // 删除学习 + { + uint8_t ch = studyCH - 20; + + uint8_t index = relay->config.study_index[ch]; + for (int i = 0; i <= index; ++i) + { + if (relay->config.study[(ch * 10) + i] == value) + { + for (int j = i; j <= index - 1; j++) + { + relay->config.study[(ch * 10) + j] = relay->config.study[(ch * 10) + (j + 1)]; + } + relay->config.study_index[ch] = --index; + Config::saveConfig(); + } + } + + Debug::AddInfo(PSTR("Received %d del to channel %d"), value, (ch) + 1); + studyCH = 0; + Led::blinkLED(200, 5); + } + else if (studyCH >= 10) // 学习 + { + uint8_t ch = studyCH - 10; + uint8_t index = relay->config.study_index[ch]; + Debug::AddInfo(PSTR("study index %d %d"), ch, index); + for (uint8_t i = 0; i < index; i++) + { + if (relay->config.study[(ch * 10) + i] == value) + { + Debug::AddInfo(PSTR("Received %d study to channel %d is has"), value, (ch) + 1); + studyCH = 0; + return; + } + } + + if (index >= MAX_STUDY_RECEIVER_NUM) + { + for (int j = 0; j < index - 1; j++) + { + relay->config.study[(ch * 10) + j] = relay->config.study[(ch * 10) + (j + 1)]; + } + relay->config.study[(ch * 10) + (MAX_STUDY_RECEIVER_NUM - 1)] = value; + } + else + { + relay->config.study[(ch * 10) + index] = value; + relay->config.study_index[ch] = ++index; + } + Config::saveConfig(); + + Debug::AddInfo(PSTR("Received %d study to channel %d"), value, (ch) + 1); + studyCH = 0; + Led::blinkLED(200, 5); + } +} +#endif \ No newline at end of file diff --git a/src/Relay.cpp b/src/Relay.cpp new file mode 100644 index 0000000..269efcd --- /dev/null +++ b/src/Relay.cpp @@ -0,0 +1,853 @@ +#include "Relay.h" +#include "RelayButton.h" +#include "RadioReceive.h" +#include "Rtc.h" + +#pragma region 继承 + +String Relay::getModuleCNName() +{ + return String(channels) + F("路开关模块"); +} + +void Relay::init() +{ + if (config.led_light == 0) + { + config.led_light = 100; + } + if (config.led_time == 0) + { + config.led_time = 2; + } + ledLight = config.led_light * 10 + 23; + + loadModule(config.module_type); + if (GPIO_PIN[GPIO_LED_POWER] != 99) + { + Led::init(GPIO_PIN[GPIO_LED_POWER], HIGH); + } + else if (GPIO_PIN[GPIO_LED_POWER_INV] != 99) + { + Led::init(GPIO_PIN[GPIO_LED_POWER_INV], LOW); + } + +#ifdef USE_RCSWITCH + if (GPIO_PIN[GPIO_RFRECV] != 99) + { + radioReceive = new RadioReceive(); + radioReceive->init(this, GPIO_PIN[GPIO_RFRECV]); + } +#endif + + channels = 0; + for (uint8_t ch = 0; ch < 4; ch++) + { + if (GPIO_PIN[GPIO_REL1 + ch] == 99) + { + continue; + } + channels++; + + pinMode(GPIO_PIN[GPIO_REL1 + ch], OUTPUT); // 继电器 + if (GPIO_PIN[GPIO_LED1 + ch] != 99) + { + pinMode(GPIO_PIN[GPIO_LED1 + ch], OUTPUT); // LED + } + } + + btns = new RelayButton[channels]; + + strcpy(powerStatTopic, Mqtt::getStatTopic(F("POWER1")).c_str()); + + if (config.led_type == 2) + { + ledTicker = new Ticker(); + } + for (uint8_t ch = 0; ch < channels; ch++) + { + if (GPIO_PIN[GPIO_KEY1 + ch] != 99) + { + btns[ch].init(this, ch, GPIO_PIN[GPIO_KEY1 + ch]); + } + + // 0:开关通电时断开 1:开关通电时闭合 2:开关通电时状态与断电前相反 3:开关通电时保持断电前状态 + if (config.power_on_state == 2) + { + switchRelay(ch, !bitRead(config.last_state, ch), false); // 开关通电时状态与断电前相反 + } + else if (config.power_on_state == 3) + { + switchRelay(ch, bitRead(config.last_state, ch), false); // 开关通电时保持断电前状态 + } + else + { + switchRelay(ch, config.power_on_state == 1, false); // 开关通电时闭合 + } + } + + checkCanLed(true); +} + +bool Relay::moduleLed() +{ +#ifdef USE_RCSWITCH + if (radioReceive && radioReceive->studyCH > 0) + { + Led::on(); + return true; + } +#endif + return false; +} + +void Relay::loop() +{ + for (size_t ch = 0; ch < channels; ch++) + { + if (GPIO_PIN[GPIO_KEY1 + ch] != 99) + { + btns[ch].loop(); + } + } + +#ifdef USE_RCSWITCH + if (radioReceive) + { + radioReceive->loop(); + } +#endif + + if (bitRead(operationFlag, 0)) + { + bitClear(operationFlag, 0); + if (perSecond % 60 == 0) + { + checkCanLed(); + } + if (config.report_interval > 0 && (perSecond % config.report_interval) == 0) + { + reportPower(); + } + } +} + +void Relay::perSecondDo() +{ + bitSet(operationFlag, 0); +} +#pragma endregion + +#pragma region 配置 + +void Relay::readConfig() +{ + Config::moduleReadConfig(MODULE_CFG_VERSION, sizeof(RelayConfigMessage), RelayConfigMessage_fields, &config); +} + +void Relay::resetConfig() +{ + Debug::AddInfo(PSTR("moduleResetConfig . . . OK")); + memset(&config, 0, sizeof(RelayConfigMessage)); + config.module_type = SupportedModules::CH3; + config.led_light = 50; + config.led_time = 3; +} + +void Relay::saveConfig(bool isEverySecond) +{ + Config::moduleSaveConfig(MODULE_CFG_VERSION, RelayConfigMessage_size, RelayConfigMessage_fields, &config); +} +#pragma endregion + +#pragma region MQTT + +void Relay::mqttCallback(String topicStr, String str) +{ + if (channels >= 1 && topicStr.endsWith("/POWER1")) + { + switchRelay(0, (str == "ON" ? true : (str == "OFF" ? false : !bitRead(lastState, 0)))); + } + else if (channels >= 2 && topicStr.endsWith("/POWER2")) + { + switchRelay(1, (str == "ON" ? true : (str == "OFF" ? false : !bitRead(lastState, 1)))); + } + else if (channels >= 3 && topicStr.endsWith("/POWER3")) + { + switchRelay(2, (str == "ON" ? true : (str == "OFF" ? false : !bitRead(lastState, 2)))); + } + else if (channels >= 4 && topicStr.endsWith("/POWER4")) + { + switchRelay(3, (str == "ON" ? true : (str == "OFF" ? false : !bitRead(lastState, 3)))); + } + else if (topicStr.endsWith("/report")) + { + reportPower(); + } +} + +void Relay::mqttConnected() +{ + strcpy(powerStatTopic, Mqtt::getStatTopic(F("POWER1")).c_str()); + if (globalConfig.mqtt.discovery) + { + mqttDiscovery(true); + } + reportPower(); +} + +void Relay::mqttDiscovery(bool isEnable) +{ + char topic[50]; + char message[500]; + + String availability = Mqtt::getTeleTopic(F("availability")); + char cmndTopic[80]; + strcpy(cmndTopic, Mqtt::getCmndTopic(F("POWER1")).c_str()); + for (size_t ch = 0; ch < channels; ch++) + { + sprintf(topic, "%s/light/%s_l%d/config", globalConfig.mqtt.discovery_prefix, UID, (ch + 1)); + if (isEnable) + { + cmndTopic[strlen(cmndTopic) - 1] = ch + 49; // 48 + 1 + ch + powerStatTopic[strlen(powerStatTopic) - 1] = ch + 49; // 48 + 1 + ch + sprintf(message, HASS_DISCOVER_RELAY, UID, (ch + 1), + cmndTopic, + powerStatTopic, + availability.c_str()); + //Debug::AddInfo(PSTR("discovery: %s - %s"), topic, message); + Mqtt::publish(topic, message, true); + } + else + { + Mqtt::publish(topic, "", true); + } + } + if (isEnable) + { + Mqtt::availability(); + } +} +#pragma endregion + +#pragma region Http + +void Relay::httpAdd(ESP8266WebServer *server) +{ + server->on(F("/relay_do"), std::bind(&Relay::httpDo, this, server)); + server->on(F("/relay_setting"), std::bind(&Relay::httpSetting, this, server)); + server->on(F("/ha"), std::bind(&Relay::httpHa, this, server)); +#ifdef USE_RCSWITCH + server->on(F("/rf_do"), std::bind(&Relay::httpRadioReceive, this, server)); +#endif +#ifdef USE_TRICOLOR + server->on(F("/downlight_setting"), std::bind(&Relay::httpDownlightSetting, this, server)); +#endif +} + +String Relay::httpGetStatus(ESP8266WebServer *server) +{ + String data; + for (size_t ch = 0; ch < channels; ch++) + { + data += ",\"relay_" + String(ch + 1) + "\":"; + data += bitRead(lastState, ch) ? 1 : 0; + } + return data.substring(1); +} + +void Relay::httpHtml(ESP8266WebServer *server) +{ + String radioJs = F(""); + + server->sendContent(page); + server->sendContent(radioJs); +} + +void Relay::httpDo(ESP8266WebServer *server) +{ + String c = server->arg(F("c")); + if (c != F("1") && c != F("2") && c != F("3") && c != F("4")) + { + server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"参数错误。\"}")); + return; + } + uint8_t ch = c.toInt() - 1; + if (ch > channels) + { + server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"继电器数量错误。\"}")); + return; + } + String str = server->arg(F("do")); + switchRelay(ch, (str == "ON" ? true : (str == "OFF" ? false : !bitRead(lastState, ch)))); + + server->send(200, F("text/html"), "{\"code\":1,\"msg\":\"操作成功\",\"data\":{" + httpGetStatus(server) + "}}"); +} + +#ifdef USE_RCSWITCH +void Relay::httpRadioReceive(ESP8266WebServer *server) +{ + if (!radioReceive) + { + server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"没有射频模块。\"}")); + return; + } + String d = server->arg(F("do")); + String c = server->arg(F("c")); + if ((d != F("s") && d != F("d") && d != F("c")) || (c != F("0") && (c.toInt() < 1 || c.toInt() > channels))) + { + server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"参数错误。\"}")); + return; + } + if (radioReceive->studyCH != 0) + { + server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"上一个操作未完成\"}")); + return; + } + if (d == F("s")) + { + radioReceive->study(c.toInt() - 1); + } + else if (d == F("d")) + { + radioReceive->del(c.toInt() - 1); + } + else if (d == F("c")) + { + if (c == F("0")) + { + radioReceive->delAll(); + } + else + { + config.study_index[c.toInt() - 1] = 0; + } + } + Config::saveConfig(); + server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"操作成功\"}")); +} +#endif + +#ifdef USE_TRICOLOR +void Relay::httpDownlightSetting(ESP8266WebServer *server) +{ + String color1 = server->arg(F("color1")); + String color2 = server->arg(F("color2")); + String color3 = server->arg(F("color3")); + if (color1 == color2 || color1 == color3 || color2 == color3) + { + server->send(200, F("text/html"), F("{\"code\":0,\"msg\":\"三色排序存在相同\"}")); + return; + } + + config.downlight_ch = server->arg(F("downlight_ch")).toInt(); + config.downlight_default = server->arg(F("default")).toInt(); + config.downlight_color[0] = color1.toInt(); + config.downlight_color[1] = color2.toInt(); + config.downlight_color[2] = color3.toInt(); + Config::saveConfig(); + server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"已经设置成功。\"}")); +} +#endif + +void Relay::httpSetting(ESP8266WebServer *server) +{ + config.power_on_state = server->arg(F("power_on_state")).toInt(); + config.report_interval = server->arg(F("report_interval")).toInt(); + + if (server->hasArg(F("power_mode"))) + { + config.power_mode = server->arg(F("power_mode")).toInt(); + } + if (server->hasArg(F("led_type"))) + { + config.led_type = server->arg(F("led_type")).toInt(); + if (config.led_type == 2 && !ledTicker) + { + ledTicker = new Ticker(); + } + } + + if (server->hasArg(F("led_start")) && server->hasArg(F("led_end"))) + { + config.led_start = server->arg(F("led_start")).toInt(); + config.led_end = server->arg(F("led_end")).toInt(); + } + + if (server->hasArg(F("led_light"))) + { + config.led_light = server->arg(F("led_light")).toInt(); + ledLight = config.led_light * 10 + 23; + } + if (server->hasArg(F("relay_led_time"))) + { + config.led_time = server->arg(F("relay_led_time")).toInt(); + if (config.led_type == 2 && ledTicker->active()) + { + ledTicker->detach(); + } + } + checkCanLed(true); + + if (server->hasArg(F("module_type")) && !server->arg(F("module_type")).equals(String(config.module_type))) + { + server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"已经更换模块类型 . . . 正在重启中。\"}")); + config.module_type = server->arg(F("module_type")).toInt(); + Config::saveConfig(); + Led::blinkLED(400, 4); + ESP.restart(); + } + else + { + Config::saveConfig(); + server->send(200, F("text/html"), F("{\"code\":1,\"msg\":\"已经设置成功。\"}")); + } +} + +void Relay::httpHa(ESP8266WebServer *server) +{ + char attachment[100]; + snprintf_P(attachment, sizeof(attachment), PSTR("attachment; filename=%s.yaml"), UID); + + server->setContentLength(CONTENT_LENGTH_UNKNOWN); + server->sendHeader(F("Content-Disposition"), attachment); + server->send(200, F("Content-Type: application/octet-stream"), ""); + + String availability = Mqtt::getTeleTopic(F("availability")); + char cmndTopic[100]; + strcpy(cmndTopic, Mqtt::getCmndTopic(F("POWER1")).c_str()); + server->sendContent(F("light:\r\n")); + for (size_t ch = 0; ch < channels; ch++) + { + cmndTopic[strlen(cmndTopic) - 1] = ch + 49; // 48 + 1 + ch + powerStatTopic[strlen(powerStatTopic) - 1] = ch + 49; // 48 + 1 + ch + server->sendContent(F(" - platform: mqtt\r\n name: \"")); + server->sendContent(UID); + server->sendContent(F("_l")); + server->sendContent(String((ch + 1))); + server->sendContent(F("\"\r\n state_topic: \"")); + server->sendContent(powerStatTopic); + server->sendContent(F("\"\r\n command_topic: \"")); + server->sendContent(cmndTopic); + server->sendContent(F("\"\r\n payload_on: \"ON\"\r\n payload_off: \"OFF\"\r\n availability_topic: \"")); + server->sendContent(availability); + server->sendContent(F("\"\r\n payload_available: \"online\"\r\n payload_not_available: \"offline\"\r\n\r\n")); + } +} +#pragma endregion + +#pragma region Led + +void Relay::ledTickerHandle() +{ + for (uint8_t ch = 0; ch < channels; ch++) + { + if (!bitRead(lastState, ch) && GPIO_PIN[GPIO_LED1 + ch] != 99) + { + analogWrite(GPIO_PIN[GPIO_LED1 + ch], ledLevel); + } + } + if (ledUp) + { + ledLevel++; + if (ledLevel >= ledLight) + { + ledUp = false; + } + } + else + { + ledLevel--; + if (ledLevel <= 50) + { + ledUp = true; + } + } +} + +void Relay::ledPWM(uint8_t ch, bool isOn) +{ + if (isOn) + { + analogWrite(GPIO_PIN[GPIO_LED1 + ch], 0); + if (ledTicker->active()) + { + for (uint8_t ch2 = 0; ch2 < channels; ch2++) + { + if (!bitRead(lastState, ch2)) + { + return; + } + } + ledTicker->detach(); + Debug::AddInfo(PSTR("ledTicker detach")); + } + } + else + { + if (!ledTicker->active()) + { + ledTicker->attach_ms(config.led_time, std::bind(&Relay::ledTickerHandle, this)); + Debug::AddInfo(PSTR("ledTicker active")); + } + } +} + +void Relay::led(uint8_t ch, bool isOn) +{ + if (config.led_type == 0 || GPIO_PIN[GPIO_LED1 + ch] == 99) + { + return; + } + + if (config.led_type == 1) + { + //digitalWrite(GPIO_PIN[GPIO_LED1 + ch], isOn ? LOW : HIGH); + analogWrite(GPIO_PIN[GPIO_LED1 + ch], isOn ? 0 : ledLight); + } + else if (config.led_type == 2) + { + ledPWM(ch, isOn); + } +} + +bool Relay::checkCanLed(bool re) +{ + bool result; + if (config.led_start != config.led_end && Rtc::rtcTime.valid) + { + uint16_t nowTime = Rtc::rtcTime.hour * 100 + Rtc::rtcTime.minute; + if (config.led_start > config.led_end) // 开始时间大于结束时间 跨日 + { + result = (nowTime >= config.led_start || nowTime < config.led_end); + } + else + { + result = (nowTime >= config.led_start && nowTime < config.led_end); + } + } + else + { + result = true; // 没有正确时间为一直亮 + } + if (result != Relay::canLed || re) + { + if ((!result || config.led_type != 2) && ledTicker && ledTicker->active()) + { + ledTicker->detach(); + Debug::AddInfo(PSTR("ledTicker detach2")); + } + Relay::canLed = result; + Debug::AddInfo(result ? PSTR("led can light") : PSTR("led can not light")); + for (uint8_t ch = 0; ch < channels; ch++) + { + if (GPIO_PIN[GPIO_LED1 + ch] != 99) + { + result &&config.led_type != 0 ? led(ch, bitRead(lastState, ch)) : analogWrite(GPIO_PIN[GPIO_LED1 + ch], 0); + } + } + } + + return result; +} +#pragma endregion + +void Relay::switchRelay(uint8_t ch, bool isOn, bool isSave) +{ + if (ch > channels) + { + Debug::AddInfo(PSTR("invalid channel: %d"), ch); + return; + } + Debug::AddInfo(PSTR("Relay %d . . . %s"), ch + 1, isOn ? "ON" : "OFF"); + + if (isOn && config.power_mode == 1) + { + for (size_t ch2 = 0; ch2 < channels; ch2++) + { + if (ch2 != ch && bitRead(lastState, ch2)) + { + switchRelay(ch2, false, isSave); + } + } + } + +#ifdef USE_TRICOLOR + colorOnOff(ch, isOn); +#endif + + bitWrite(lastState, ch, isOn); + digitalWrite(GPIO_PIN[GPIO_REL1 + ch], isOn ? HIGH : LOW); + + powerStatTopic[strlen(powerStatTopic) - 1] = ch + 49; // 48 + 1 + ch + Mqtt::publish(powerStatTopic, isOn ? "ON" : "OFF", globalConfig.mqtt.retain); + + if (isSave && config.power_on_state > 0) + { + bitWrite(config.last_state, ch, isOn); + Config::delaySaveConfig(10); + } + if (Relay::canLed) + { + led(ch, isOn); + } +} + +void Relay::loadModule(uint8_t module) +{ + for (uint16_t i = 0; i < sizeof(GPIO_PIN); i++) + { + GPIO_PIN[i] = 99; + } + + mytmplt m = Modules[module]; + uint8_t j = 0; + for (uint8_t i = 0; i < sizeof(m.io); i++) + { + if (6 == i) + { + j = 9; + } + if (8 == i) + { + j = 12; + } + GPIO_PIN[m.io[i]] = j; + j++; + } +} + +void Relay::reportPower() +{ + for (size_t ch = 0; ch < channels; ch++) + { + powerStatTopic[strlen(powerStatTopic) - 1] = ch + 49; // 48 + 1 + ch + Mqtt::publish(powerStatTopic, bitRead(lastState, ch) ? "ON" : "OFF", globalConfig.mqtt.retain); + } +} + +#ifdef USE_TRICOLOR +void Relay::colorOnOff(uint8_t ch, bool isOn) +{ + if (config.downlight_ch == ch + 1 && bitRead(lastState, ch) != isOn) // 三色筒灯 + { + if (!isOn) + { + colorOffTime = millis(); + return; + } + + if (millis() > colorOffTime + (2 * 1000)) // 关灯超过5秒 + { + if (config.downlight_default == 0) + { + config.downlight_index = 0; + } + else + { + uint8_t def = config.downlight_default == 4 ? config.downlight_color[config.downlight_index] : config.downlight_default; + for (size_t i = 0; i < 3; i++) + { + if (def == config.downlight_color[i]) + { + config.downlight_index = i; + break; + } + digitalWrite(GPIO_PIN[GPIO_REL1 + ch], HIGH); + delay(105); + digitalWrite(GPIO_PIN[GPIO_REL1 + ch], LOW); + delay(105); + } + } + } + else + { + config.downlight_index = (config.downlight_index + 1) % 3; + } + Debug::AddInfo(PSTR("cur color: %d"), config.downlight_index); + } +} +#endif \ No newline at end of file diff --git a/src/RelayButton.cpp b/src/RelayButton.cpp new file mode 100644 index 0000000..9b82903 --- /dev/null +++ b/src/RelayButton.cpp @@ -0,0 +1,69 @@ +#include "Relay.h" +#include "RadioReceive.h" +#include "RelayButton.h" + +void RelayButton::init(Relay *_relay, uint8_t _ch, uint8_t _io) +{ + ch = _ch; + io = _io; + relay = _relay; + pinMode(io, INPUT_PULLUP); + if (digitalRead(io)) + { + setStateFlag(DEBOUNCED_STATE | UNSTABLE_STATE); + } +} + +void RelayButton::loop() +{ + currentState = digitalRead(io); + if (currentState != getStateFlag(UNSTABLE_STATE)) + { + timingStart = millis(); + toggleStateFlag(UNSTABLE_STATE); + } + else if (millis() - timingStart >= debounceTime) + { + if (currentState != getStateFlag(DEBOUNCED_STATE)) + { + timingStart = millis(); + toggleStateFlag(DEBOUNCED_STATE); + + switchCount += 1; + intervalStart = millis(); + + if (millis() > lastTime + 300) + { + relay->switchRelay(ch, !bitRead(relay->lastState, ch), true); + lastTime = millis(); + } + } + } + + // 如果经过的时间大于超时并且计数大于0,则填充并重置计数 + if (switchCount > 0 && (millis() - intervalStart) > specialFunctionTimeout) + { + Led::led(200); + Debug::AddInfo(PSTR("switchCount %d : %d"), ch + 1, switchCount); + +#ifdef USE_RCSWITCH + if (switchCount == 10 && relay->radioReceive) + { + relay->radioReceive->study(ch); + } + else if (switchCount == 12 && relay->radioReceive) + { + relay->radioReceive->del(ch); + } + else if (switchCount == 16 && relay->radioReceive) + { + relay->radioReceive->delAll(); + } +#endif + if (switchCount == 20) + { + Wifi::setupWifiManager(false); + } + switchCount = 0; + } +} diff --git a/src/RelayConfig.pb.c b/src/RelayConfig.pb.c new file mode 100644 index 0000000..d1a79fa --- /dev/null +++ b/src/RelayConfig.pb.c @@ -0,0 +1,35 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.3.9.4 at Thu Feb 20 15:26:50 2020. */ + +#include "RelayConfig.pb.h" + +/* @@protoc_insertion_point(includes) */ +#if PB_PROTO_HEADER_VERSION != 30 +#error Regenerate this file with the current version of nanopb generator. +#endif + + + +const pb_field_t RelayConfigMessage_fields[18] = { + PB_FIELD( 1, UINT32 , SINGULAR, STATIC , FIRST, RelayConfigMessage, led_type, led_type, 0), + PB_FIELD( 2, UINT32 , SINGULAR, STATIC , OTHER, RelayConfigMessage, led_start, led_type, 0), + PB_FIELD( 3, UINT32 , SINGULAR, STATIC , OTHER, RelayConfigMessage, led_end, led_start, 0), + PB_FIELD( 4, UINT32 , SINGULAR, STATIC , OTHER, RelayConfigMessage, power_on_state, led_end, 0), + PB_FIELD( 5, UINT32 , SINGULAR, STATIC , OTHER, RelayConfigMessage, last_state, power_on_state, 0), + PB_REPEATED_FIXED_COUNT( 6, UINT32 , OTHER, RelayConfigMessage, study_index, last_state, 0), + PB_REPEATED_FIXED_COUNT( 7, UINT32 , OTHER, RelayConfigMessage, study, study_index, 0), + PB_FIELD( 8, UINT32 , SINGULAR, STATIC , OTHER, RelayConfigMessage, led_light, study, 0), + PB_FIELD( 9, UINT32 , SINGULAR, STATIC , OTHER, RelayConfigMessage, led_time, led_light, 0), + PB_FIELD( 10, UINT32 , SINGULAR, STATIC , OTHER, RelayConfigMessage, downlight_ch, led_time, 0), + PB_FIELD( 11, UINT32 , SINGULAR, STATIC , OTHER, RelayConfigMessage, downlight_index, downlight_ch, 0), + PB_REPEATED_FIXED_COUNT( 12, UINT32 , OTHER, RelayConfigMessage, downlight_color, downlight_index, 0), + PB_FIELD( 13, UINT32 , SINGULAR, STATIC , OTHER, RelayConfigMessage, downlight_default, downlight_color, 0), + PB_FIELD( 14, UINT32 , SINGULAR, STATIC , OTHER, RelayConfigMessage, downlight_interval, downlight_default, 0), + PB_FIELD( 19, UINT32 , SINGULAR, STATIC , OTHER, RelayConfigMessage, power_mode, downlight_interval, 0), + PB_FIELD( 20, UINT32 , SINGULAR, STATIC , OTHER, RelayConfigMessage, module_type, power_mode, 0), + PB_FIELD( 21, UINT32 , SINGULAR, STATIC , OTHER, RelayConfigMessage, report_interval, module_type, 0), + PB_LAST_FIELD +}; + + +/* @@protoc_insertion_point(eof) */ diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..a1e711b --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,23 @@ +#include +#include +#include +#include +#include +#include + +#include "Framework.h" +#include "Relay.h" + +void setup() +{ + Framework::one(115200); + + module = new Relay(); + + Framework::setup(); +} + +void loop() +{ + Framework::loop(); +} \ No newline at end of file