├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── include └── README ├── lib └── json11 │ ├── CMakeLists.txt │ ├── LICENSE.txt │ ├── Makefile │ ├── README.md │ ├── json11.cpp │ ├── json11.hpp │ ├── json11.pc.in │ └── test.cpp ├── platformio.ini ├── schematic.pdf ├── src ├── display_task.cpp ├── display_task.h ├── event.h ├── gif_player.cpp ├── gif_player.h ├── logger.h ├── main.cpp ├── main_task.cpp ├── main_task.h ├── semaphore_guard.h └── task.h └── test └── README /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .pioenvs 3 | .piolibdeps 4 | .vscode/.browse.c_cpp.db* 5 | .vscode/c_cpp_properties.json 6 | .vscode/launch.json 7 | .vscode/settings.json 8 | 9 | # KiCAD files 10 | *.000 11 | *.bak 12 | *.bck 13 | *.kicad_pcb-bak 14 | *.sch-bak 15 | *.net 16 | *.dsn 17 | fp-info-cache 18 | 19 | src/boot_gif.h 20 | secrets.h -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Reference code for my homemade Nintendo Switch ornament: 2 | 3 | 4 | 5 | Plays animated gifs from the SD card using the `bitbank2/AnimatedGIF` library and `TFT_eSPI` display driver. 6 | 7 | Wifi and other settings (time zone, debug log visibility) are configured via a `config.json` file at the root of the SD card. Firmware can be updated by putting a `firmware.bin` file at the root of the SD card, or over wifi by entering the credits screen (click the right button) which enables ArduinoOTA. 8 | -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /lib/json11/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8) 2 | if (CMAKE_VERSION VERSION_LESS "3") 3 | project(json11 CXX) 4 | else() 5 | cmake_policy(SET CMP0048 NEW) 6 | project(json11 VERSION 1.0.0 LANGUAGES CXX) 7 | endif() 8 | 9 | enable_testing() 10 | 11 | option(JSON11_BUILD_TESTS "Build unit tests" OFF) 12 | option(JSON11_ENABLE_DR1467_CANARY "Enable canary test for DR 1467" OFF) 13 | 14 | if(CMAKE_VERSION VERSION_LESS "3") 15 | add_definitions(-std=c++11) 16 | else() 17 | set(CMAKE_CXX_STANDARD 11) 18 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 19 | endif() 20 | 21 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 22 | set(CMAKE_INSTALL_PREFIX /usr) 23 | endif() 24 | 25 | add_library(json11 json11.cpp) 26 | target_include_directories(json11 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 27 | target_compile_options(json11 28 | PRIVATE -fPIC -fno-rtti -fno-exceptions -Wall) 29 | 30 | # Set warning flags, which may vary per platform 31 | include(CheckCXXCompilerFlag) 32 | set(_possible_warnings_flags /W4 /WX -Wextra -Werror) 33 | foreach(_warning_flag ${_possible_warnings_flags}) 34 | unset(_flag_supported) 35 | CHECK_CXX_COMPILER_FLAG(${_warning_flag} _flag_supported) 36 | if(${_flag_supported}) 37 | target_compile_options(json11 PRIVATE ${_warning_flag}) 38 | endif() 39 | endforeach() 40 | 41 | configure_file("json11.pc.in" "json11.pc" @ONLY) 42 | 43 | if (JSON11_BUILD_TESTS) 44 | 45 | # enable test for DR1467, described here: https://llvm.org/bugs/show_bug.cgi?id=23812 46 | if(JSON11_ENABLE_DR1467_CANARY) 47 | add_definitions(-D JSON11_ENABLE_DR1467_CANARY=1) 48 | else() 49 | add_definitions(-D JSON11_ENABLE_DR1467_CANARY=0) 50 | endif() 51 | 52 | add_executable(json11_test test.cpp) 53 | target_link_libraries(json11_test json11) 54 | endif() 55 | 56 | install(TARGETS json11 DESTINATION lib/${CMAKE_LIBRARY_ARCHITECTURE}) 57 | install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/json11.hpp" DESTINATION include/${CMAKE_LIBRARY_ARCHITECTURE}) 58 | install(FILES "${CMAKE_CURRENT_BINARY_DIR}/json11.pc" DESTINATION lib/${CMAKE_LIBRARY_ARCHITECTURE}/pkgconfig) 59 | -------------------------------------------------------------------------------- /lib/json11/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Dropbox, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/json11/Makefile: -------------------------------------------------------------------------------- 1 | # Environment variable to enable or disable code which demonstrates the behavior change 2 | # in Xcode 7 / Clang 3.7, introduced by DR1467 and described here: 3 | # https://llvm.org/bugs/show_bug.cgi?id=23812 4 | # Defaults to on in order to act as a warning to anyone who's unaware of the issue. 5 | ifneq ($(JSON11_ENABLE_DR1467_CANARY),) 6 | CANARY_ARGS = -DJSON11_ENABLE_DR1467_CANARY=$(JSON11_ENABLE_DR1467_CANARY) 7 | endif 8 | 9 | test: json11.cpp json11.hpp test.cpp 10 | $(CXX) $(CANARY_ARGS) -O -std=c++11 json11.cpp test.cpp -o test -fno-rtti -fno-exceptions 11 | 12 | clean: 13 | if [ -e test ]; then rm test; fi 14 | 15 | .PHONY: clean 16 | -------------------------------------------------------------------------------- /lib/json11/README.md: -------------------------------------------------------------------------------- 1 | json11 2 | ------ 3 | 4 | json11 is a tiny JSON library for C++11, providing JSON parsing and serialization. 5 | 6 | The core object provided by the library is json11::Json. A Json object represents any JSON 7 | value: null, bool, number (int or double), string (std::string), array (std::vector), or 8 | object (std::map). 9 | 10 | Json objects act like values. They can be assigned, copied, moved, compared for equality or 11 | order, and so on. There are also helper methods Json::dump, to serialize a Json to a string, and 12 | Json::parse (static) to parse a std::string as a Json object. 13 | 14 | It's easy to make a JSON object with C++11's new initializer syntax: 15 | 16 | Json my_json = Json::object { 17 | { "key1", "value1" }, 18 | { "key2", false }, 19 | { "key3", Json::array { 1, 2, 3 } }, 20 | }; 21 | std::string json_str = my_json.dump(); 22 | 23 | There are also implicit constructors that allow standard and user-defined types to be 24 | automatically converted to JSON. For example: 25 | 26 | class Point { 27 | public: 28 | int x; 29 | int y; 30 | Point (int x, int y) : x(x), y(y) {} 31 | Json to_json() const { return Json::array { x, y }; } 32 | }; 33 | 34 | std::vector points = { { 1, 2 }, { 10, 20 }, { 100, 200 } }; 35 | std::string points_json = Json(points).dump(); 36 | 37 | JSON values can have their values queried and inspected: 38 | 39 | Json json = Json::array { Json::object { { "k", "v" } } }; 40 | std::string str = json[0]["k"].string_value(); 41 | 42 | For more documentation see json11.hpp. 43 | -------------------------------------------------------------------------------- /lib/json11/json11.cpp: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2013 Dropbox, Inc. 2 | * 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. 20 | */ 21 | 22 | #include "json11.hpp" 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | namespace json11 { 30 | 31 | static const int max_depth = 200; 32 | 33 | using std::string; 34 | using std::vector; 35 | using std::map; 36 | using std::make_shared; 37 | using std::initializer_list; 38 | using std::move; 39 | 40 | /* Helper for representing null - just a do-nothing struct, plus comparison 41 | * operators so the helpers in JsonValue work. We can't use nullptr_t because 42 | * it may not be orderable. 43 | */ 44 | struct NullStruct { 45 | bool operator==(NullStruct) const { return true; } 46 | bool operator<(NullStruct) const { return false; } 47 | }; 48 | 49 | /* * * * * * * * * * * * * * * * * * * * 50 | * Serialization 51 | */ 52 | 53 | static void dump(NullStruct, string &out) { 54 | out += "null"; 55 | } 56 | 57 | static void dump(double value, string &out) { 58 | if (std::isfinite(value)) { 59 | char buf[32]; 60 | snprintf(buf, sizeof buf, "%.17g", value); 61 | out += buf; 62 | } else { 63 | out += "null"; 64 | } 65 | } 66 | 67 | static void dump(int value, string &out) { 68 | char buf[32]; 69 | snprintf(buf, sizeof buf, "%d", value); 70 | out += buf; 71 | } 72 | 73 | static void dump(bool value, string &out) { 74 | out += value ? "true" : "false"; 75 | } 76 | 77 | static void dump(const string &value, string &out) { 78 | out += '"'; 79 | for (size_t i = 0; i < value.length(); i++) { 80 | const char ch = value[i]; 81 | if (ch == '\\') { 82 | out += "\\\\"; 83 | } else if (ch == '"') { 84 | out += "\\\""; 85 | } else if (ch == '\b') { 86 | out += "\\b"; 87 | } else if (ch == '\f') { 88 | out += "\\f"; 89 | } else if (ch == '\n') { 90 | out += "\\n"; 91 | } else if (ch == '\r') { 92 | out += "\\r"; 93 | } else if (ch == '\t') { 94 | out += "\\t"; 95 | } else if (static_cast(ch) <= 0x1f) { 96 | char buf[8]; 97 | snprintf(buf, sizeof buf, "\\u%04x", ch); 98 | out += buf; 99 | } else if (static_cast(ch) == 0xe2 && static_cast(value[i+1]) == 0x80 100 | && static_cast(value[i+2]) == 0xa8) { 101 | out += "\\u2028"; 102 | i += 2; 103 | } else if (static_cast(ch) == 0xe2 && static_cast(value[i+1]) == 0x80 104 | && static_cast(value[i+2]) == 0xa9) { 105 | out += "\\u2029"; 106 | i += 2; 107 | } else { 108 | out += ch; 109 | } 110 | } 111 | out += '"'; 112 | } 113 | 114 | static void dump(const Json::array &values, string &out) { 115 | bool first = true; 116 | out += "["; 117 | for (const auto &value : values) { 118 | if (!first) 119 | out += ", "; 120 | value.dump(out); 121 | first = false; 122 | } 123 | out += "]"; 124 | } 125 | 126 | static void dump(const Json::object &values, string &out) { 127 | bool first = true; 128 | out += "{"; 129 | for (const auto &kv : values) { 130 | if (!first) 131 | out += ", "; 132 | dump(kv.first, out); 133 | out += ": "; 134 | kv.second.dump(out); 135 | first = false; 136 | } 137 | out += "}"; 138 | } 139 | 140 | void Json::dump(string &out) const { 141 | m_ptr->dump(out); 142 | } 143 | 144 | /* * * * * * * * * * * * * * * * * * * * 145 | * Value wrappers 146 | */ 147 | 148 | template 149 | class Value : public JsonValue { 150 | protected: 151 | 152 | // Constructors 153 | explicit Value(const T &value) : m_value(value) {} 154 | explicit Value(T &&value) : m_value(move(value)) {} 155 | 156 | // Get type tag 157 | Json::Type type() const override { 158 | return tag; 159 | } 160 | 161 | // Comparisons 162 | bool equals(const JsonValue * other) const override { 163 | return m_value == static_cast *>(other)->m_value; 164 | } 165 | bool less(const JsonValue * other) const override { 166 | return m_value < static_cast *>(other)->m_value; 167 | } 168 | 169 | const T m_value; 170 | void dump(string &out) const override { json11::dump(m_value, out); } 171 | }; 172 | 173 | class JsonDouble final : public Value { 174 | double number_value() const override { return m_value; } 175 | int int_value() const override { return static_cast(m_value); } 176 | bool equals(const JsonValue * other) const override { return m_value == other->number_value(); } 177 | bool less(const JsonValue * other) const override { return m_value < other->number_value(); } 178 | public: 179 | explicit JsonDouble(double value) : Value(value) {} 180 | }; 181 | 182 | class JsonInt final : public Value { 183 | double number_value() const override { return m_value; } 184 | int int_value() const override { return m_value; } 185 | bool equals(const JsonValue * other) const override { return m_value == other->number_value(); } 186 | bool less(const JsonValue * other) const override { return m_value < other->number_value(); } 187 | public: 188 | explicit JsonInt(int value) : Value(value) {} 189 | }; 190 | 191 | class JsonBoolean final : public Value { 192 | bool bool_value() const override { return m_value; } 193 | public: 194 | explicit JsonBoolean(bool value) : Value(value) {} 195 | }; 196 | 197 | class JsonString final : public Value { 198 | const string &string_value() const override { return m_value; } 199 | public: 200 | explicit JsonString(const string &value) : Value(value) {} 201 | explicit JsonString(string &&value) : Value(move(value)) {} 202 | }; 203 | 204 | class JsonArray final : public Value { 205 | const Json::array &array_items() const override { return m_value; } 206 | const Json & operator[](size_t i) const override; 207 | public: 208 | explicit JsonArray(const Json::array &value) : Value(value) {} 209 | explicit JsonArray(Json::array &&value) : Value(move(value)) {} 210 | }; 211 | 212 | class JsonObject final : public Value { 213 | const Json::object &object_items() const override { return m_value; } 214 | const Json & operator[](const string &key) const override; 215 | public: 216 | explicit JsonObject(const Json::object &value) : Value(value) {} 217 | explicit JsonObject(Json::object &&value) : Value(move(value)) {} 218 | }; 219 | 220 | class JsonNull final : public Value { 221 | public: 222 | JsonNull() : Value({}) {} 223 | }; 224 | 225 | /* * * * * * * * * * * * * * * * * * * * 226 | * Static globals - static-init-safe 227 | */ 228 | struct Statics { 229 | const std::shared_ptr null = make_shared(); 230 | const std::shared_ptr t = make_shared(true); 231 | const std::shared_ptr f = make_shared(false); 232 | const string empty_string; 233 | const vector empty_vector; 234 | const map empty_map; 235 | Statics() {} 236 | }; 237 | 238 | static const Statics & statics() { 239 | static const Statics s {}; 240 | return s; 241 | } 242 | 243 | static const Json & static_null() { 244 | // This has to be separate, not in Statics, because Json() accesses statics().null. 245 | static const Json json_null; 246 | return json_null; 247 | } 248 | 249 | /* * * * * * * * * * * * * * * * * * * * 250 | * Constructors 251 | */ 252 | 253 | Json::Json() noexcept : m_ptr(statics().null) {} 254 | Json::Json(std::nullptr_t) noexcept : m_ptr(statics().null) {} 255 | Json::Json(double value) : m_ptr(make_shared(value)) {} 256 | Json::Json(int value) : m_ptr(make_shared(value)) {} 257 | Json::Json(bool value) : m_ptr(value ? statics().t : statics().f) {} 258 | Json::Json(const string &value) : m_ptr(make_shared(value)) {} 259 | Json::Json(string &&value) : m_ptr(make_shared(move(value))) {} 260 | Json::Json(const char * value) : m_ptr(make_shared(value)) {} 261 | Json::Json(const Json::array &values) : m_ptr(make_shared(values)) {} 262 | Json::Json(Json::array &&values) : m_ptr(make_shared(move(values))) {} 263 | Json::Json(const Json::object &values) : m_ptr(make_shared(values)) {} 264 | Json::Json(Json::object &&values) : m_ptr(make_shared(move(values))) {} 265 | 266 | /* * * * * * * * * * * * * * * * * * * * 267 | * Accessors 268 | */ 269 | 270 | Json::Type Json::type() const { return m_ptr->type(); } 271 | double Json::number_value() const { return m_ptr->number_value(); } 272 | int Json::int_value() const { return m_ptr->int_value(); } 273 | bool Json::bool_value() const { return m_ptr->bool_value(); } 274 | const string & Json::string_value() const { return m_ptr->string_value(); } 275 | const vector & Json::array_items() const { return m_ptr->array_items(); } 276 | const map & Json::object_items() const { return m_ptr->object_items(); } 277 | const Json & Json::operator[] (size_t i) const { return (*m_ptr)[i]; } 278 | const Json & Json::operator[] (const string &key) const { return (*m_ptr)[key]; } 279 | 280 | double JsonValue::number_value() const { return 0; } 281 | int JsonValue::int_value() const { return 0; } 282 | bool JsonValue::bool_value() const { return false; } 283 | const string & JsonValue::string_value() const { return statics().empty_string; } 284 | const vector & JsonValue::array_items() const { return statics().empty_vector; } 285 | const map & JsonValue::object_items() const { return statics().empty_map; } 286 | const Json & JsonValue::operator[] (size_t) const { return static_null(); } 287 | const Json & JsonValue::operator[] (const string &) const { return static_null(); } 288 | 289 | const Json & JsonObject::operator[] (const string &key) const { 290 | auto iter = m_value.find(key); 291 | return (iter == m_value.end()) ? static_null() : iter->second; 292 | } 293 | const Json & JsonArray::operator[] (size_t i) const { 294 | if (i >= m_value.size()) return static_null(); 295 | else return m_value[i]; 296 | } 297 | 298 | /* * * * * * * * * * * * * * * * * * * * 299 | * Comparison 300 | */ 301 | 302 | bool Json::operator== (const Json &other) const { 303 | if (m_ptr == other.m_ptr) 304 | return true; 305 | if (m_ptr->type() != other.m_ptr->type()) 306 | return false; 307 | 308 | return m_ptr->equals(other.m_ptr.get()); 309 | } 310 | 311 | bool Json::operator< (const Json &other) const { 312 | if (m_ptr == other.m_ptr) 313 | return false; 314 | if (m_ptr->type() != other.m_ptr->type()) 315 | return m_ptr->type() < other.m_ptr->type(); 316 | 317 | return m_ptr->less(other.m_ptr.get()); 318 | } 319 | 320 | /* * * * * * * * * * * * * * * * * * * * 321 | * Parsing 322 | */ 323 | 324 | /* esc(c) 325 | * 326 | * Format char c suitable for printing in an error message. 327 | */ 328 | static inline string esc(char c) { 329 | char buf[12]; 330 | if (static_cast(c) >= 0x20 && static_cast(c) <= 0x7f) { 331 | snprintf(buf, sizeof buf, "'%c' (%d)", c, c); 332 | } else { 333 | snprintf(buf, sizeof buf, "(%d)", c); 334 | } 335 | return string(buf); 336 | } 337 | 338 | static inline bool in_range(long x, long lower, long upper) { 339 | return (x >= lower && x <= upper); 340 | } 341 | 342 | namespace { 343 | /* JsonParser 344 | * 345 | * Object that tracks all state of an in-progress parse. 346 | */ 347 | struct JsonParser final { 348 | 349 | /* State 350 | */ 351 | const string &str; 352 | size_t i; 353 | string &err; 354 | bool failed; 355 | const JsonParse strategy; 356 | 357 | /* fail(msg, err_ret = Json()) 358 | * 359 | * Mark this parse as failed. 360 | */ 361 | Json fail(string &&msg) { 362 | return fail(move(msg), Json()); 363 | } 364 | 365 | template 366 | T fail(string &&msg, const T err_ret) { 367 | if (!failed) 368 | err = std::move(msg); 369 | failed = true; 370 | return err_ret; 371 | } 372 | 373 | /* consume_whitespace() 374 | * 375 | * Advance until the current character is non-whitespace. 376 | */ 377 | void consume_whitespace() { 378 | while (str[i] == ' ' || str[i] == '\r' || str[i] == '\n' || str[i] == '\t') 379 | i++; 380 | } 381 | 382 | /* consume_comment() 383 | * 384 | * Advance comments (c-style inline and multiline). 385 | */ 386 | bool consume_comment() { 387 | bool comment_found = false; 388 | if (str[i] == '/') { 389 | i++; 390 | if (i == str.size()) 391 | return fail("unexpected end of input after start of comment", false); 392 | if (str[i] == '/') { // inline comment 393 | i++; 394 | // advance until next line, or end of input 395 | while (i < str.size() && str[i] != '\n') { 396 | i++; 397 | } 398 | comment_found = true; 399 | } 400 | else if (str[i] == '*') { // multiline comment 401 | i++; 402 | if (i > str.size()-2) 403 | return fail("unexpected end of input inside multi-line comment", false); 404 | // advance until closing tokens 405 | while (!(str[i] == '*' && str[i+1] == '/')) { 406 | i++; 407 | if (i > str.size()-2) 408 | return fail( 409 | "unexpected end of input inside multi-line comment", false); 410 | } 411 | i += 2; 412 | comment_found = true; 413 | } 414 | else 415 | return fail("malformed comment", false); 416 | } 417 | return comment_found; 418 | } 419 | 420 | /* consume_garbage() 421 | * 422 | * Advance until the current character is non-whitespace and non-comment. 423 | */ 424 | void consume_garbage() { 425 | consume_whitespace(); 426 | if(strategy == JsonParse::COMMENTS) { 427 | bool comment_found = false; 428 | do { 429 | comment_found = consume_comment(); 430 | if (failed) return; 431 | consume_whitespace(); 432 | } 433 | while(comment_found); 434 | } 435 | } 436 | 437 | /* get_next_token() 438 | * 439 | * Return the next non-whitespace character. If the end of the input is reached, 440 | * flag an error and return 0. 441 | */ 442 | char get_next_token() { 443 | consume_garbage(); 444 | if (failed) return static_cast(0); 445 | if (i == str.size()) 446 | return fail("unexpected end of input", static_cast(0)); 447 | 448 | return str[i++]; 449 | } 450 | 451 | /* encode_utf8(pt, out) 452 | * 453 | * Encode pt as UTF-8 and add it to out. 454 | */ 455 | void encode_utf8(long pt, string & out) { 456 | if (pt < 0) 457 | return; 458 | 459 | if (pt < 0x80) { 460 | out += static_cast(pt); 461 | } else if (pt < 0x800) { 462 | out += static_cast((pt >> 6) | 0xC0); 463 | out += static_cast((pt & 0x3F) | 0x80); 464 | } else if (pt < 0x10000) { 465 | out += static_cast((pt >> 12) | 0xE0); 466 | out += static_cast(((pt >> 6) & 0x3F) | 0x80); 467 | out += static_cast((pt & 0x3F) | 0x80); 468 | } else { 469 | out += static_cast((pt >> 18) | 0xF0); 470 | out += static_cast(((pt >> 12) & 0x3F) | 0x80); 471 | out += static_cast(((pt >> 6) & 0x3F) | 0x80); 472 | out += static_cast((pt & 0x3F) | 0x80); 473 | } 474 | } 475 | 476 | /* parse_string() 477 | * 478 | * Parse a string, starting at the current position. 479 | */ 480 | string parse_string() { 481 | string out; 482 | long last_escaped_codepoint = -1; 483 | while (true) { 484 | if (i == str.size()) 485 | return fail("unexpected end of input in string", ""); 486 | 487 | char ch = str[i++]; 488 | 489 | if (ch == '"') { 490 | encode_utf8(last_escaped_codepoint, out); 491 | return out; 492 | } 493 | 494 | if (in_range(ch, 0, 0x1f)) 495 | return fail("unescaped " + esc(ch) + " in string", ""); 496 | 497 | // The usual case: non-escaped characters 498 | if (ch != '\\') { 499 | encode_utf8(last_escaped_codepoint, out); 500 | last_escaped_codepoint = -1; 501 | out += ch; 502 | continue; 503 | } 504 | 505 | // Handle escapes 506 | if (i == str.size()) 507 | return fail("unexpected end of input in string", ""); 508 | 509 | ch = str[i++]; 510 | 511 | if (ch == 'u') { 512 | // Extract 4-byte escape sequence 513 | string esc = str.substr(i, 4); 514 | // Explicitly check length of the substring. The following loop 515 | // relies on std::string returning the terminating NUL when 516 | // accessing str[length]. Checking here reduces brittleness. 517 | if (esc.length() < 4) { 518 | return fail("bad \\u escape: " + esc, ""); 519 | } 520 | for (size_t j = 0; j < 4; j++) { 521 | if (!in_range(esc[j], 'a', 'f') && !in_range(esc[j], 'A', 'F') 522 | && !in_range(esc[j], '0', '9')) 523 | return fail("bad \\u escape: " + esc, ""); 524 | } 525 | 526 | long codepoint = strtol(esc.data(), nullptr, 16); 527 | 528 | // JSON specifies that characters outside the BMP shall be encoded as a pair 529 | // of 4-hex-digit \u escapes encoding their surrogate pair components. Check 530 | // whether we're in the middle of such a beast: the previous codepoint was an 531 | // escaped lead (high) surrogate, and this is a trail (low) surrogate. 532 | if (in_range(last_escaped_codepoint, 0xD800, 0xDBFF) 533 | && in_range(codepoint, 0xDC00, 0xDFFF)) { 534 | // Reassemble the two surrogate pairs into one astral-plane character, per 535 | // the UTF-16 algorithm. 536 | encode_utf8((((last_escaped_codepoint - 0xD800) << 10) 537 | | (codepoint - 0xDC00)) + 0x10000, out); 538 | last_escaped_codepoint = -1; 539 | } else { 540 | encode_utf8(last_escaped_codepoint, out); 541 | last_escaped_codepoint = codepoint; 542 | } 543 | 544 | i += 4; 545 | continue; 546 | } 547 | 548 | encode_utf8(last_escaped_codepoint, out); 549 | last_escaped_codepoint = -1; 550 | 551 | if (ch == 'b') { 552 | out += '\b'; 553 | } else if (ch == 'f') { 554 | out += '\f'; 555 | } else if (ch == 'n') { 556 | out += '\n'; 557 | } else if (ch == 'r') { 558 | out += '\r'; 559 | } else if (ch == 't') { 560 | out += '\t'; 561 | } else if (ch == '"' || ch == '\\' || ch == '/') { 562 | out += ch; 563 | } else { 564 | return fail("invalid escape character " + esc(ch), ""); 565 | } 566 | } 567 | } 568 | 569 | /* parse_number() 570 | * 571 | * Parse a double. 572 | */ 573 | Json parse_number() { 574 | size_t start_pos = i; 575 | 576 | if (str[i] == '-') 577 | i++; 578 | 579 | // Integer part 580 | if (str[i] == '0') { 581 | i++; 582 | if (in_range(str[i], '0', '9')) 583 | return fail("leading 0s not permitted in numbers"); 584 | } else if (in_range(str[i], '1', '9')) { 585 | i++; 586 | while (in_range(str[i], '0', '9')) 587 | i++; 588 | } else { 589 | return fail("invalid " + esc(str[i]) + " in number"); 590 | } 591 | 592 | if (str[i] != '.' && str[i] != 'e' && str[i] != 'E' 593 | && (i - start_pos) <= static_cast(std::numeric_limits::digits10)) { 594 | return std::atoi(str.c_str() + start_pos); 595 | } 596 | 597 | // Decimal part 598 | if (str[i] == '.') { 599 | i++; 600 | if (!in_range(str[i], '0', '9')) 601 | return fail("at least one digit required in fractional part"); 602 | 603 | while (in_range(str[i], '0', '9')) 604 | i++; 605 | } 606 | 607 | // Exponent part 608 | if (str[i] == 'e' || str[i] == 'E') { 609 | i++; 610 | 611 | if (str[i] == '+' || str[i] == '-') 612 | i++; 613 | 614 | if (!in_range(str[i], '0', '9')) 615 | return fail("at least one digit required in exponent"); 616 | 617 | while (in_range(str[i], '0', '9')) 618 | i++; 619 | } 620 | 621 | return std::strtod(str.c_str() + start_pos, nullptr); 622 | } 623 | 624 | /* expect(str, res) 625 | * 626 | * Expect that 'str' starts at the character that was just read. If it does, advance 627 | * the input and return res. If not, flag an error. 628 | */ 629 | Json expect(const string &expected, Json res) { 630 | assert(i != 0); 631 | i--; 632 | if (str.compare(i, expected.length(), expected) == 0) { 633 | i += expected.length(); 634 | return res; 635 | } else { 636 | return fail("parse error: expected " + expected + ", got " + str.substr(i, expected.length())); 637 | } 638 | } 639 | 640 | /* parse_json() 641 | * 642 | * Parse a JSON object. 643 | */ 644 | Json parse_json(int depth) { 645 | if (depth > max_depth) { 646 | return fail("exceeded maximum nesting depth"); 647 | } 648 | 649 | char ch = get_next_token(); 650 | if (failed) 651 | return Json(); 652 | 653 | if (ch == '-' || (ch >= '0' && ch <= '9')) { 654 | i--; 655 | return parse_number(); 656 | } 657 | 658 | if (ch == 't') 659 | return expect("true", true); 660 | 661 | if (ch == 'f') 662 | return expect("false", false); 663 | 664 | if (ch == 'n') 665 | return expect("null", Json()); 666 | 667 | if (ch == '"') 668 | return parse_string(); 669 | 670 | if (ch == '{') { 671 | map data; 672 | ch = get_next_token(); 673 | if (ch == '}') 674 | return data; 675 | 676 | while (1) { 677 | if (ch != '"') 678 | return fail("expected '\"' in object, got " + esc(ch)); 679 | 680 | string key = parse_string(); 681 | if (failed) 682 | return Json(); 683 | 684 | ch = get_next_token(); 685 | if (ch != ':') 686 | return fail("expected ':' in object, got " + esc(ch)); 687 | 688 | data[std::move(key)] = parse_json(depth + 1); 689 | if (failed) 690 | return Json(); 691 | 692 | ch = get_next_token(); 693 | if (ch == '}') 694 | break; 695 | if (ch != ',') 696 | return fail("expected ',' in object, got " + esc(ch)); 697 | 698 | ch = get_next_token(); 699 | } 700 | return data; 701 | } 702 | 703 | if (ch == '[') { 704 | vector data; 705 | ch = get_next_token(); 706 | if (ch == ']') 707 | return data; 708 | 709 | while (1) { 710 | i--; 711 | data.push_back(parse_json(depth + 1)); 712 | if (failed) 713 | return Json(); 714 | 715 | ch = get_next_token(); 716 | if (ch == ']') 717 | break; 718 | if (ch != ',') 719 | return fail("expected ',' in list, got " + esc(ch)); 720 | 721 | ch = get_next_token(); 722 | (void)ch; 723 | } 724 | return data; 725 | } 726 | 727 | return fail("expected value, got " + esc(ch)); 728 | } 729 | }; 730 | }//namespace { 731 | 732 | Json Json::parse(const string &in, string &err, JsonParse strategy) { 733 | JsonParser parser { in, 0, err, false, strategy }; 734 | Json result = parser.parse_json(0); 735 | 736 | // Check for any trailing garbage 737 | parser.consume_garbage(); 738 | if (parser.failed) 739 | return Json(); 740 | if (parser.i != in.size()) 741 | return parser.fail("unexpected trailing " + esc(in[parser.i])); 742 | 743 | return result; 744 | } 745 | 746 | // Documented in json11.hpp 747 | vector Json::parse_multi(const string &in, 748 | std::string::size_type &parser_stop_pos, 749 | string &err, 750 | JsonParse strategy) { 751 | JsonParser parser { in, 0, err, false, strategy }; 752 | parser_stop_pos = 0; 753 | vector json_vec; 754 | while (parser.i != in.size() && !parser.failed) { 755 | json_vec.push_back(parser.parse_json(0)); 756 | if (parser.failed) 757 | break; 758 | 759 | // Check for another object 760 | parser.consume_garbage(); 761 | if (parser.failed) 762 | break; 763 | parser_stop_pos = parser.i; 764 | } 765 | return json_vec; 766 | } 767 | 768 | /* * * * * * * * * * * * * * * * * * * * 769 | * Shape-checking 770 | */ 771 | 772 | bool Json::has_shape(const shape & types, string & err) const { 773 | if (!is_object()) { 774 | err = "expected JSON object, got " + dump(); 775 | return false; 776 | } 777 | 778 | const auto& obj_items = object_items(); 779 | for (auto & item : types) { 780 | const auto it = obj_items.find(item.first); 781 | if (it == obj_items.cend() || it->second.type() != item.second) { 782 | err = "bad type for " + item.first + " in " + dump(); 783 | return false; 784 | } 785 | } 786 | 787 | return true; 788 | } 789 | 790 | } // namespace json11 791 | -------------------------------------------------------------------------------- /lib/json11/json11.hpp: -------------------------------------------------------------------------------- 1 | /* json11 2 | * 3 | * json11 is a tiny JSON library for C++11, providing JSON parsing and serialization. 4 | * 5 | * The core object provided by the library is json11::Json. A Json object represents any JSON 6 | * value: null, bool, number (int or double), string (std::string), array (std::vector), or 7 | * object (std::map). 8 | * 9 | * Json objects act like values: they can be assigned, copied, moved, compared for equality or 10 | * order, etc. There are also helper methods Json::dump, to serialize a Json to a string, and 11 | * Json::parse (static) to parse a std::string as a Json object. 12 | * 13 | * Internally, the various types of Json object are represented by the JsonValue class 14 | * hierarchy. 15 | * 16 | * A note on numbers - JSON specifies the syntax of number formatting but not its semantics, 17 | * so some JSON implementations distinguish between integers and floating-point numbers, while 18 | * some don't. In json11, we choose the latter. Because some JSON implementations (namely 19 | * Javascript itself) treat all numbers as the same type, distinguishing the two leads 20 | * to JSON that will be *silently* changed by a round-trip through those implementations. 21 | * Dangerous! To avoid that risk, json11 stores all numbers as double internally, but also 22 | * provides integer helpers. 23 | * 24 | * Fortunately, double-precision IEEE754 ('double') can precisely store any integer in the 25 | * range +/-2^53, which includes every 'int' on most systems. (Timestamps often use int64 26 | * or long long to avoid the Y2038K problem; a double storing microseconds since some epoch 27 | * will be exact for +/- 275 years.) 28 | */ 29 | 30 | /* Copyright (c) 2013 Dropbox, Inc. 31 | * 32 | * Permission is hereby granted, free of charge, to any person obtaining a copy 33 | * of this software and associated documentation files (the "Software"), to deal 34 | * in the Software without restriction, including without limitation the rights 35 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 36 | * copies of the Software, and to permit persons to whom the Software is 37 | * furnished to do so, subject to the following conditions: 38 | * 39 | * The above copyright notice and this permission notice shall be included in 40 | * all copies or substantial portions of the Software. 41 | * 42 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 43 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 44 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 45 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 46 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 47 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 48 | * THE SOFTWARE. 49 | */ 50 | 51 | #pragma once 52 | 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | 59 | #ifdef _MSC_VER 60 | #if _MSC_VER <= 1800 // VS 2013 61 | #ifndef noexcept 62 | #define noexcept throw() 63 | #endif 64 | 65 | #ifndef snprintf 66 | #define snprintf _snprintf_s 67 | #endif 68 | #endif 69 | #endif 70 | 71 | namespace json11 { 72 | 73 | enum JsonParse { 74 | STANDARD, COMMENTS 75 | }; 76 | 77 | class JsonValue; 78 | 79 | class Json final { 80 | public: 81 | // Types 82 | enum Type { 83 | NUL, NUMBER, BOOL, STRING, ARRAY, OBJECT 84 | }; 85 | 86 | // Array and object typedefs 87 | typedef std::vector array; 88 | typedef std::map object; 89 | 90 | // Constructors for the various types of JSON value. 91 | Json() noexcept; // NUL 92 | Json(std::nullptr_t) noexcept; // NUL 93 | Json(double value); // NUMBER 94 | Json(int value); // NUMBER 95 | Json(bool value); // BOOL 96 | Json(const std::string &value); // STRING 97 | Json(std::string &&value); // STRING 98 | Json(const char * value); // STRING 99 | Json(const array &values); // ARRAY 100 | Json(array &&values); // ARRAY 101 | Json(const object &values); // OBJECT 102 | Json(object &&values); // OBJECT 103 | 104 | // Implicit constructor: anything with a to_json() function. 105 | template 106 | Json(const T & t) : Json(t.to_json()) {} 107 | 108 | // Implicit constructor: map-like objects (std::map, std::unordered_map, etc) 109 | template ().begin()->first)>::value 111 | && std::is_constructible().begin()->second)>::value, 112 | int>::type = 0> 113 | Json(const M & m) : Json(object(m.begin(), m.end())) {} 114 | 115 | // Implicit constructor: vector-like objects (std::list, std::vector, std::set, etc) 116 | template ().begin())>::value, 118 | int>::type = 0> 119 | Json(const V & v) : Json(array(v.begin(), v.end())) {} 120 | 121 | // This prevents Json(some_pointer) from accidentally producing a bool. Use 122 | // Json(bool(some_pointer)) if that behavior is desired. 123 | Json(void *) = delete; 124 | 125 | // Accessors 126 | Type type() const; 127 | 128 | bool is_null() const { return type() == NUL; } 129 | bool is_number() const { return type() == NUMBER; } 130 | bool is_bool() const { return type() == BOOL; } 131 | bool is_string() const { return type() == STRING; } 132 | bool is_array() const { return type() == ARRAY; } 133 | bool is_object() const { return type() == OBJECT; } 134 | 135 | // Return the enclosed value if this is a number, 0 otherwise. Note that json11 does not 136 | // distinguish between integer and non-integer numbers - number_value() and int_value() 137 | // can both be applied to a NUMBER-typed object. 138 | double number_value() const; 139 | int int_value() const; 140 | 141 | // Return the enclosed value if this is a boolean, false otherwise. 142 | bool bool_value() const; 143 | // Return the enclosed string if this is a string, "" otherwise. 144 | const std::string &string_value() const; 145 | // Return the enclosed std::vector if this is an array, or an empty vector otherwise. 146 | const array &array_items() const; 147 | // Return the enclosed std::map if this is an object, or an empty map otherwise. 148 | const object &object_items() const; 149 | 150 | // Return a reference to arr[i] if this is an array, Json() otherwise. 151 | const Json & operator[](size_t i) const; 152 | // Return a reference to obj[key] if this is an object, Json() otherwise. 153 | const Json & operator[](const std::string &key) const; 154 | 155 | // Serialize. 156 | void dump(std::string &out) const; 157 | std::string dump() const { 158 | std::string out; 159 | dump(out); 160 | return out; 161 | } 162 | 163 | // Parse. If parse fails, return Json() and assign an error message to err. 164 | static Json parse(const std::string & in, 165 | std::string & err, 166 | JsonParse strategy = JsonParse::STANDARD); 167 | static Json parse(const char * in, 168 | std::string & err, 169 | JsonParse strategy = JsonParse::STANDARD) { 170 | if (in) { 171 | return parse(std::string(in), err, strategy); 172 | } else { 173 | err = "null input"; 174 | return nullptr; 175 | } 176 | } 177 | // Parse multiple objects, concatenated or separated by whitespace 178 | static std::vector parse_multi( 179 | const std::string & in, 180 | std::string::size_type & parser_stop_pos, 181 | std::string & err, 182 | JsonParse strategy = JsonParse::STANDARD); 183 | 184 | static inline std::vector parse_multi( 185 | const std::string & in, 186 | std::string & err, 187 | JsonParse strategy = JsonParse::STANDARD) { 188 | std::string::size_type parser_stop_pos; 189 | return parse_multi(in, parser_stop_pos, err, strategy); 190 | } 191 | 192 | bool operator== (const Json &rhs) const; 193 | bool operator< (const Json &rhs) const; 194 | bool operator!= (const Json &rhs) const { return !(*this == rhs); } 195 | bool operator<= (const Json &rhs) const { return !(rhs < *this); } 196 | bool operator> (const Json &rhs) const { return (rhs < *this); } 197 | bool operator>= (const Json &rhs) const { return !(*this < rhs); } 198 | 199 | /* has_shape(types, err) 200 | * 201 | * Return true if this is a JSON object and, for each item in types, has a field of 202 | * the given type. If not, return false and set err to a descriptive message. 203 | */ 204 | typedef std::initializer_list> shape; 205 | bool has_shape(const shape & types, std::string & err) const; 206 | 207 | private: 208 | std::shared_ptr m_ptr; 209 | }; 210 | 211 | // Internal class hierarchy - JsonValue objects are not exposed to users of this API. 212 | class JsonValue { 213 | protected: 214 | friend class Json; 215 | friend class JsonInt; 216 | friend class JsonDouble; 217 | virtual Json::Type type() const = 0; 218 | virtual bool equals(const JsonValue * other) const = 0; 219 | virtual bool less(const JsonValue * other) const = 0; 220 | virtual void dump(std::string &out) const = 0; 221 | virtual double number_value() const; 222 | virtual int int_value() const; 223 | virtual bool bool_value() const; 224 | virtual const std::string &string_value() const; 225 | virtual const Json::array &array_items() const; 226 | virtual const Json &operator[](size_t i) const; 227 | virtual const Json::object &object_items() const; 228 | virtual const Json &operator[](const std::string &key) const; 229 | virtual ~JsonValue() {} 230 | }; 231 | 232 | } // namespace json11 233 | -------------------------------------------------------------------------------- /lib/json11/json11.pc.in: -------------------------------------------------------------------------------- 1 | prefix=@CMAKE_INSTALL_PREFIX@ 2 | libdir=${prefix}/lib/@CMAKE_LIBRARY_ARCHITECTURE@ 3 | includedir=${prefix}/include/@CMAKE_LIBRARY_ARCHITECTURE@ 4 | 5 | Name: @PROJECT_NAME@ 6 | Description: json11 is a tiny JSON library for C++11, providing JSON parsing and serialization. 7 | Version: @PROJECT_VERSION@ 8 | Libs: -L${libdir} -ljson11 9 | Cflags: -I${includedir} 10 | -------------------------------------------------------------------------------- /lib/json11/test.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Define JSON11_TEST_CUSTOM_CONFIG to 1 if you want to build this tester into 3 | * your own unit-test framework rather than a stand-alone program. By setting 4 | * The values of the variables included below, you can insert your own custom 5 | * code into this file as it builds, in order to make it into a test case for 6 | * your favorite framework. 7 | */ 8 | #if !JSON11_TEST_CUSTOM_CONFIG 9 | #define JSON11_TEST_CPP_PREFIX_CODE 10 | #define JSON11_TEST_CPP_SUFFIX_CODE 11 | #define JSON11_TEST_STANDALONE_MAIN 1 12 | #define JSON11_TEST_CASE(name) static void name() 13 | #define JSON11_TEST_ASSERT(b) assert(b) 14 | #ifdef NDEBUG 15 | #undef NDEBUG//at now assert will work even in Release build 16 | #endif 17 | #endif // JSON11_TEST_CUSTOM_CONFIG 18 | 19 | /* 20 | * Enable or disable code which demonstrates the behavior change in Xcode 7 / Clang 3.7, 21 | * introduced by DR1467 and described here: https://github.com/dropbox/json11/issues/86 22 | * Defaults to off since it doesn't appear the standards committee is likely to act 23 | * on this, so it needs to be considered normal behavior. 24 | */ 25 | #ifndef JSON11_ENABLE_DR1467_CANARY 26 | #define JSON11_ENABLE_DR1467_CANARY 0 27 | #endif 28 | 29 | /* 30 | * Beginning of standard source file, which makes use of the customizations above. 31 | */ 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include "json11.hpp" 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | 45 | // Insert user-defined prefix code (includes, function declarations, etc) 46 | // to set up a custom test suite 47 | JSON11_TEST_CPP_PREFIX_CODE 48 | 49 | using namespace json11; 50 | using std::string; 51 | 52 | // Check that Json has the properties we want. 53 | #define CHECK_TRAIT(x) static_assert(std::x::value, #x) 54 | CHECK_TRAIT(is_nothrow_constructible); 55 | CHECK_TRAIT(is_nothrow_default_constructible); 56 | CHECK_TRAIT(is_copy_constructible); 57 | CHECK_TRAIT(is_nothrow_move_constructible); 58 | CHECK_TRAIT(is_copy_assignable); 59 | CHECK_TRAIT(is_nothrow_move_assignable); 60 | CHECK_TRAIT(is_nothrow_destructible); 61 | 62 | JSON11_TEST_CASE(json11_test) { 63 | const string simple_test = 64 | R"({"k1":"v1", "k2":42, "k3":["a",123,true,false,null]})"; 65 | 66 | string err; 67 | const auto json = Json::parse(simple_test, err); 68 | 69 | std::cout << "k1: " << json["k1"].string_value() << "\n"; 70 | std::cout << "k3: " << json["k3"].dump() << "\n"; 71 | 72 | for (auto &k : json["k3"].array_items()) { 73 | std::cout << " - " << k.dump() << "\n"; 74 | } 75 | 76 | string comment_test = R"({ 77 | // comment /* with nested comment */ 78 | "a": 1, 79 | // comment 80 | // continued 81 | "b": "text", 82 | /* multi 83 | line 84 | comment 85 | // line-comment-inside-multiline-comment 86 | */ 87 | // and single-line comment 88 | // and single-line comment /* multiline inside single line */ 89 | "c": [1, 2, 3] 90 | // and single-line comment at end of object 91 | })"; 92 | 93 | string err_comment; 94 | auto json_comment = Json::parse( 95 | comment_test, err_comment, JsonParse::COMMENTS); 96 | JSON11_TEST_ASSERT(!json_comment.is_null()); 97 | JSON11_TEST_ASSERT(err_comment.empty()); 98 | 99 | comment_test = "{\"a\": 1}//trailing line comment"; 100 | json_comment = Json::parse( 101 | comment_test, err_comment, JsonParse::COMMENTS); 102 | JSON11_TEST_ASSERT(!json_comment.is_null()); 103 | JSON11_TEST_ASSERT(err_comment.empty()); 104 | 105 | comment_test = "{\"a\": 1}/*trailing multi-line comment*/"; 106 | json_comment = Json::parse( 107 | comment_test, err_comment, JsonParse::COMMENTS); 108 | JSON11_TEST_ASSERT(!json_comment.is_null()); 109 | JSON11_TEST_ASSERT(err_comment.empty()); 110 | 111 | string failing_comment_test = "{\n/* unterminated comment\n\"a\": 1,\n}"; 112 | string err_failing_comment; 113 | auto json_failing_comment = Json::parse( 114 | failing_comment_test, err_failing_comment, JsonParse::COMMENTS); 115 | JSON11_TEST_ASSERT(json_failing_comment.is_null()); 116 | JSON11_TEST_ASSERT(!err_failing_comment.empty()); 117 | 118 | failing_comment_test = "{\n/* unterminated trailing comment }"; 119 | json_failing_comment = Json::parse( 120 | failing_comment_test, err_failing_comment, JsonParse::COMMENTS); 121 | JSON11_TEST_ASSERT(json_failing_comment.is_null()); 122 | JSON11_TEST_ASSERT(!err_failing_comment.empty()); 123 | 124 | failing_comment_test = "{\n/ / bad comment }"; 125 | json_failing_comment = Json::parse( 126 | failing_comment_test, err_failing_comment, JsonParse::COMMENTS); 127 | JSON11_TEST_ASSERT(json_failing_comment.is_null()); 128 | JSON11_TEST_ASSERT(!err_failing_comment.empty()); 129 | 130 | failing_comment_test = "{// bad comment }"; 131 | json_failing_comment = Json::parse( 132 | failing_comment_test, err_failing_comment, JsonParse::COMMENTS); 133 | JSON11_TEST_ASSERT(json_failing_comment.is_null()); 134 | JSON11_TEST_ASSERT(!err_failing_comment.empty()); 135 | 136 | failing_comment_test = "{\n\"a\": 1\n}/"; 137 | json_failing_comment = Json::parse( 138 | failing_comment_test, err_failing_comment, JsonParse::COMMENTS); 139 | JSON11_TEST_ASSERT(json_failing_comment.is_null()); 140 | JSON11_TEST_ASSERT(!err_failing_comment.empty()); 141 | 142 | failing_comment_test = "{/* bad\ncomment *}"; 143 | json_failing_comment = Json::parse( 144 | failing_comment_test, err_failing_comment, JsonParse::COMMENTS); 145 | JSON11_TEST_ASSERT(json_failing_comment.is_null()); 146 | JSON11_TEST_ASSERT(!err_failing_comment.empty()); 147 | 148 | std::list l1 { 1, 2, 3 }; 149 | std::vector l2 { 1, 2, 3 }; 150 | std::set l3 { 1, 2, 3 }; 151 | JSON11_TEST_ASSERT(Json(l1) == Json(l2)); 152 | JSON11_TEST_ASSERT(Json(l2) == Json(l3)); 153 | 154 | std::map m1 { { "k1", "v1" }, { "k2", "v2" } }; 155 | std::unordered_map m2 { { "k1", "v1" }, { "k2", "v2" } }; 156 | JSON11_TEST_ASSERT(Json(m1) == Json(m2)); 157 | 158 | // Json literals 159 | const Json obj = Json::object({ 160 | { "k1", "v1" }, 161 | { "k2", 42.0 }, 162 | { "k3", Json::array({ "a", 123.0, true, false, nullptr }) }, 163 | }); 164 | 165 | std::cout << "obj: " << obj.dump() << "\n"; 166 | JSON11_TEST_ASSERT(obj.dump() == "{\"k1\": \"v1\", \"k2\": 42, \"k3\": [\"a\", 123, true, false, null]}"); 167 | 168 | JSON11_TEST_ASSERT(Json("a").number_value() == 0); 169 | JSON11_TEST_ASSERT(Json("a").string_value() == "a"); 170 | JSON11_TEST_ASSERT(Json().number_value() == 0); 171 | 172 | JSON11_TEST_ASSERT(obj == json); 173 | JSON11_TEST_ASSERT(Json(42) == Json(42.0)); 174 | JSON11_TEST_ASSERT(Json(42) != Json(42.1)); 175 | 176 | const string unicode_escape_test = 177 | R"([ "blah\ud83d\udca9blah\ud83dblah\udca9blah\u0000blah\u1234" ])"; 178 | 179 | const char utf8[] = "blah" "\xf0\x9f\x92\xa9" "blah" "\xed\xa0\xbd" "blah" 180 | "\xed\xb2\xa9" "blah" "\0" "blah" "\xe1\x88\xb4"; 181 | 182 | Json uni = Json::parse(unicode_escape_test, err); 183 | JSON11_TEST_ASSERT(uni[0].string_value().size() == (sizeof utf8) - 1); 184 | JSON11_TEST_ASSERT(std::memcmp(uni[0].string_value().data(), utf8, sizeof utf8) == 0); 185 | 186 | // Demonstrates the behavior change in Xcode 7 / Clang 3.7, introduced by DR1467 187 | // and described here: https://llvm.org/bugs/show_bug.cgi?id=23812 188 | if (JSON11_ENABLE_DR1467_CANARY) { 189 | Json nested_array = Json::array { Json::array { 1, 2, 3 } }; 190 | JSON11_TEST_ASSERT(nested_array.is_array()); 191 | JSON11_TEST_ASSERT(nested_array.array_items().size() == 1); 192 | JSON11_TEST_ASSERT(nested_array.array_items()[0].is_array()); 193 | JSON11_TEST_ASSERT(nested_array.array_items()[0].array_items().size() == 3); 194 | } 195 | 196 | { 197 | const std::string good_json = R"( {"k1" : "v1"})"; 198 | const std::string bad_json1 = good_json + " {"; 199 | const std::string bad_json2 = good_json + R"({"k2":"v2", "k3":[)"; 200 | struct TestMultiParse { 201 | std::string input; 202 | std::string::size_type expect_parser_stop_pos; 203 | size_t expect_not_empty_elms_count; 204 | Json expect_parse_res; 205 | } tests[] = { 206 | {" {", 0, 0, {}}, 207 | {good_json, good_json.size(), 1, Json(std::map{ { "k1", "v1" } })}, 208 | {bad_json1, good_json.size() + 1, 1, Json(std::map{ { "k1", "v1" } })}, 209 | {bad_json2, good_json.size(), 1, Json(std::map{ { "k1", "v1" } })}, 210 | {"{}", 2, 1, Json::object{}}, 211 | }; 212 | for (const auto &tst : tests) { 213 | std::string::size_type parser_stop_pos; 214 | std::string err; 215 | auto res = Json::parse_multi(tst.input, parser_stop_pos, err); 216 | JSON11_TEST_ASSERT(parser_stop_pos == tst.expect_parser_stop_pos); 217 | JSON11_TEST_ASSERT( 218 | (size_t)std::count_if(res.begin(), res.end(), 219 | [](const Json& j) { return !j.is_null(); }) 220 | == tst.expect_not_empty_elms_count); 221 | if (!res.empty()) { 222 | JSON11_TEST_ASSERT(tst.expect_parse_res == res[0]); 223 | } 224 | } 225 | } 226 | 227 | Json my_json = Json::object { 228 | { "key1", "value1" }, 229 | { "key2", false }, 230 | { "key3", Json::array { 1, 2, 3 } }, 231 | }; 232 | std::string json_obj_str = my_json.dump(); 233 | std::cout << "json_obj_str: " << json_obj_str << "\n"; 234 | JSON11_TEST_ASSERT(json_obj_str == "{\"key1\": \"value1\", \"key2\": false, \"key3\": [1, 2, 3]}"); 235 | 236 | class Point { 237 | public: 238 | int x; 239 | int y; 240 | Point (int x, int y) : x(x), y(y) {} 241 | Json to_json() const { return Json::array { x, y }; } 242 | }; 243 | 244 | std::vector points = { { 1, 2 }, { 10, 20 }, { 100, 200 } }; 245 | std::string points_json = Json(points).dump(); 246 | std::cout << "points_json: " << points_json << "\n"; 247 | JSON11_TEST_ASSERT(points_json == "[[1, 2], [10, 20], [100, 200]]"); 248 | 249 | JSON11_TEST_ASSERT(((Json)(Json::object { { "foo", nullptr } })).has_shape({ { "foo", Json::NUL } }, err) == true); 250 | JSON11_TEST_ASSERT(((Json)(Json::object { { "foo", 1234567 } })).has_shape({ { "foo", Json::NUL } }, err) == false); 251 | JSON11_TEST_ASSERT(((Json)(Json::object { { "bar", 1234567 } })).has_shape({ { "foo", Json::NUL } }, err) == false); 252 | 253 | } 254 | 255 | #if JSON11_TEST_STANDALONE_MAIN 256 | 257 | static void parse_from_stdin() { 258 | string buf; 259 | string line; 260 | while (std::getline(std::cin, line)) { 261 | buf += line + "\n"; 262 | } 263 | 264 | string err; 265 | auto json = Json::parse(buf, err); 266 | if (!err.empty()) { 267 | printf("Failed: %s\n", err.c_str()); 268 | } else { 269 | printf("Result: %s\n", json.dump().c_str()); 270 | } 271 | } 272 | 273 | int main(int argc, char **argv) { 274 | if (argc == 2 && argv[1] == string("--stdin")) { 275 | parse_from_stdin(); 276 | return 0; 277 | } 278 | 279 | json11_test(); 280 | } 281 | 282 | #endif // JSON11_TEST_STANDALONE_MAIN 283 | 284 | // Insert user-defined suffix code (function definitions, etc) 285 | // to set up a custom test suite 286 | JSON11_TEST_CPP_SUFFIX_CODE 287 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:main] 12 | platform = espressif32 13 | board = esp32doit-devkit-v1 14 | framework = arduino 15 | monitor_speed = 921600 16 | monitor_flags = 17 | --eol=CRLF 18 | --echo 19 | --filter=esp32_exception_decoder 20 | lib_deps = 21 | TFT_eSPI@2.3.84 22 | bitbank2/AnimatedGIF @ ^1.4.4 23 | bxparks/AceButton @ ^1.9.1 24 | 25 | build_type = release 26 | board_build.partitions = default_8MB.csv 27 | 28 | build_flags = 29 | -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG 30 | 31 | ; TFT_eSPI setup: 32 | -DUSER_SETUP_LOADED=1 33 | -DST7789_DRIVER=1 34 | -DCGRAM_OFFSET=1 35 | -DTFT_WIDTH=135 36 | -DTFT_HEIGHT=240 37 | -DTFT_MISO=-1 38 | -DTFT_MOSI=22 39 | -DTFT_SCLK=21 40 | -DTFT_CS=25 41 | -DTFT_DC=19 42 | -DTFT_RST=5 43 | -DLOAD_GLCD=1 44 | -DLOAD_GFXFF=1 45 | -DSPI_FREQUENCY=40000000 46 | 47 | [env:mainOTA] 48 | extends = env:main 49 | 50 | upload_protocol = espota 51 | upload_port = switchornament.local 52 | upload_flags = 53 | --auth="hunter2" 54 | -------------------------------------------------------------------------------- /schematic.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottbez1/SwitchOrnamentReference/f606f0883ff3d889ef474662fd0d1aa929718e33/schematic.pdf -------------------------------------------------------------------------------- /src/display_task.cpp: -------------------------------------------------------------------------------- 1 | #include "display_task.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include "gif_player.h" 12 | 13 | using namespace json11; 14 | 15 | #define PIN_LCD_BACKLIGHT 27 16 | 17 | #define PIN_SD_DAT1 4 18 | #define PIN_SD_DAT2 12 19 | 20 | DisplayTask::DisplayTask(MainTask& main_task, const uint8_t task_core) : Task{"Display", 8192, 1, task_core}, Logger(), main_task_(main_task) { 21 | log_queue_ = xQueueCreate(10, sizeof(std::string *)); 22 | assert(log_queue_ != NULL); 23 | 24 | event_queue_ = xQueueCreate(10, sizeof(Event)); 25 | assert(event_queue_ != NULL); 26 | } 27 | 28 | int DisplayTask::enumerateGifs(const char* basePath, std::vector& out_files) { 29 | int amount = 0; 30 | File GifRootFolder = SD_MMC.open(basePath); 31 | if(!GifRootFolder){ 32 | log_n("Failed to open directory"); 33 | return 0; 34 | } 35 | 36 | if(!GifRootFolder.isDirectory()){ 37 | log_n("Not a directory"); 38 | return 0; 39 | } 40 | 41 | File file = GifRootFolder.openNextFile(); 42 | 43 | while( file ) { 44 | if(!file.isDirectory()) { 45 | out_files.push_back( file.name() ); 46 | amount++; 47 | file.close(); 48 | } 49 | file = GifRootFolder.openNextFile(); 50 | } 51 | GifRootFolder.close(); 52 | log_n("Found %d GIF files", amount); 53 | return amount; 54 | } 55 | 56 | 57 | // perform the actual update from a given stream 58 | bool DisplayTask::performUpdate(Stream &updateSource, size_t updateSize) { 59 | if (Update.begin(updateSize)) { 60 | size_t written = Update.writeStream(updateSource); 61 | if (written == updateSize) { 62 | Serial.println("Written : " + String(written) + " successfully"); 63 | } 64 | else { 65 | Serial.println("Written only : " + String(written) + "/" + String(updateSize) + ". Retry?"); 66 | } 67 | if (Update.end()) { 68 | Serial.println("OTA done!"); 69 | if (Update.isFinished()) { 70 | Serial.println("Update successfully completed. Rebooting."); 71 | tft_.fillScreen(TFT_BLACK); 72 | tft_.drawString("Update successful!", 0, 0); 73 | return true; 74 | } 75 | else { 76 | Serial.println("Update not finished? Something went wrong!"); 77 | tft_.fillScreen(TFT_BLACK); 78 | tft_.drawString("Update error: unknown", 0, 0); 79 | } 80 | } 81 | else { 82 | uint8_t error = Update.getError(); 83 | Serial.println("Error Occurred. Error #: " + String(error)); 84 | tft_.fillScreen(TFT_BLACK); 85 | tft_.drawString("Update error: " + String(error), 0, 0); 86 | } 87 | 88 | } 89 | else 90 | { 91 | Serial.println("Not enough space to begin OTA"); 92 | tft_.fillScreen(TFT_BLACK); 93 | tft_.drawString("Not enough space", 0, 0); 94 | } 95 | return false; 96 | } 97 | 98 | // check given FS for valid firmware.bin and perform update if available 99 | bool DisplayTask::updateFromFS(fs::FS &fs) { 100 | tft_.fillScreen(TFT_BLACK); 101 | tft_.setTextDatum(TL_DATUM); 102 | 103 | File updateBin = fs.open("/firmware.bin"); 104 | if (updateBin) { 105 | if(updateBin.isDirectory()){ 106 | Serial.println("Error, firmware.bin is not a file"); 107 | updateBin.close(); 108 | return false; 109 | } 110 | 111 | size_t updateSize = updateBin.size(); 112 | 113 | bool update_successful = false; 114 | if (updateSize > 0) { 115 | Serial.println("Try to start update"); 116 | digitalWrite(PIN_LCD_BACKLIGHT, HIGH); 117 | tft_.fillScreen(TFT_BLACK); 118 | tft_.drawString("Starting update...", 0, 0); 119 | delay(1000); 120 | update_successful = performUpdate(updateBin, updateSize); 121 | } 122 | else { 123 | Serial.println("Error, file is empty"); 124 | } 125 | 126 | updateBin.close(); 127 | fs.remove("/firmware.bin"); 128 | 129 | // Leave some time to read the update result message 130 | delay(5000); 131 | return update_successful; 132 | } 133 | else { 134 | Serial.println("No firmware.bin at sd root"); 135 | return false; 136 | } 137 | } 138 | 139 | void DisplayTask::run() { 140 | pinMode(PIN_LCD_BACKLIGHT, OUTPUT); 141 | pinMode(PIN_SD_DAT1, INPUT_PULLUP); 142 | pinMode(PIN_SD_DAT2, INPUT_PULLUP); 143 | 144 | tft_.begin(); 145 | #ifdef USE_DMA 146 | tft_.initDMA(); 147 | #endif 148 | tft_.setRotation(1); 149 | tft_.fillScreen(TFT_BLACK); 150 | 151 | bool isblinked = false; 152 | while(! SD_MMC.begin("/sdcard", false) ) { 153 | digitalWrite(PIN_LCD_BACKLIGHT, HIGH); 154 | log_n("SD Card mount failed!"); 155 | isblinked = !isblinked; 156 | if( isblinked ) { 157 | tft_.setTextColor( TFT_WHITE, TFT_BLACK ); 158 | } else { 159 | tft_.setTextColor( TFT_BLACK, TFT_WHITE ); 160 | } 161 | tft_.setTextDatum(TC_DATUM); 162 | tft_.drawString( "INSERT SD", tft_.width()/2, tft_.height()/2 ); 163 | 164 | delay( 300 ); 165 | } 166 | 167 | log_n("SD Card mounted!"); 168 | 169 | if (updateFromFS(SD_MMC)) { 170 | ESP.restart(); 171 | } 172 | 173 | // ##################################################### 174 | // CHANGES ABOVE THIS LINE MAY BREAK FIRMWARE UPDATES!!! 175 | // ##################################################### 176 | 177 | main_task_.setLogger(this); 178 | 179 | // Load config from SD card 180 | File configFile = SD_MMC.open("/config.json"); 181 | if (configFile) { 182 | if(configFile.isDirectory()){ 183 | log("Error, config.json is not a file"); 184 | } else { 185 | char data[512]; 186 | size_t data_len = configFile.readBytes(data, sizeof(data) - 1); 187 | data[data_len] = 0; 188 | 189 | std::string err; 190 | Json json = Json::parse(data, err); 191 | if (err.empty()) { 192 | show_log_ = json["show_log"].bool_value(); 193 | const char* ssid = json["ssid"].string_value().c_str(); 194 | const char* password = json["password"].string_value().c_str(); 195 | Serial.printf("Wifi info: %s %s\n", ssid, password); 196 | 197 | const char* tz = json["timezone"].string_value().c_str(); 198 | Serial.printf("Timezone: %s\n", tz); 199 | 200 | main_task_.setConfig(ssid, password, tz); 201 | } else { 202 | log("Error parsing wifi credentials! " + String(err.c_str())); 203 | } 204 | } 205 | configFile.close(); 206 | } else { 207 | log("Missing config file!"); 208 | } 209 | 210 | // Delay to avoid brownout while wifi is starting 211 | delay(500); 212 | 213 | GifPlayer::begin(&tft_); 214 | 215 | if (GifPlayer::start("/gifs/boot.gif")) { 216 | GifPlayer::play_frame(nullptr); 217 | delay(50); 218 | digitalWrite(PIN_LCD_BACKLIGHT, HIGH); 219 | delay(200); 220 | while (GifPlayer::play_frame(nullptr)) { 221 | yield(); 222 | } 223 | digitalWrite(PIN_LCD_BACKLIGHT, LOW); 224 | delay(500); 225 | GifPlayer::stop(); 226 | } 227 | 228 | std::vector main_gifs; 229 | std::vector christmas_gifs; 230 | 231 | int num_main_gifs = enumerateGifs( "/gifs/main", main_gifs); 232 | int num_christmas_gifs = enumerateGifs( "/gifs/christmas", christmas_gifs); 233 | int current_file = -1; 234 | const char* current_file_name = ""; 235 | uint32_t minimum_loop_duration = 0; 236 | uint32_t start_millis = UINT32_MAX; 237 | 238 | bool last_christmas; // I gave you my heart... 239 | 240 | main_task_.registerEventQueue(event_queue_); 241 | 242 | State state = State::CHOOSE_GIF; 243 | int frame_delay = 0; 244 | uint32_t last_frame = 0; 245 | while (1) { 246 | bool left_button = false; 247 | bool right_button = false; 248 | Event event; 249 | if (xQueueReceive(event_queue_, &event, 0)) { 250 | switch (event.type) { 251 | case EventType::BUTTON: 252 | if (event.button.event == ace_button::AceButton::kEventPressed) { 253 | if (event.button.button_id == BUTTON_ID_LEFT) { 254 | left_button = true; 255 | } else if (event.button.button_id == BUTTON_ID_RIGHT) { 256 | right_button = true; 257 | } 258 | } 259 | break; 260 | } 261 | } 262 | handleLogRendering(); 263 | switch (state) { 264 | case State::CHOOSE_GIF: 265 | Serial.println("Choose gif"); 266 | if (millis() - start_millis > minimum_loop_duration) { 267 | // Only change the file if we've exceeded the minimum loop duration 268 | if (isChristmas()) { 269 | if (num_christmas_gifs > 0) { 270 | current_file_name = christmas_gifs[current_file++ % num_christmas_gifs].c_str(); 271 | minimum_loop_duration = 30000; 272 | Serial.printf("Chose christmas gif: %s\n", current_file_name); 273 | } else { 274 | continue; 275 | } 276 | } else { 277 | if (num_main_gifs > 0) { 278 | int next_file = current_file; 279 | while (num_main_gifs > 1 && next_file == current_file) { 280 | next_file = random(num_main_gifs); 281 | } 282 | current_file = next_file; 283 | current_file_name = main_gifs[current_file].c_str(); 284 | minimum_loop_duration = 0; 285 | Serial.printf("Chose gif: %s\n", current_file_name); 286 | } else { 287 | continue; 288 | } 289 | } 290 | start_millis = millis(); 291 | } 292 | if (!GifPlayer::start(current_file_name)) { 293 | continue; 294 | } 295 | last_frame = millis(); 296 | GifPlayer::play_frame(&frame_delay); 297 | delay(50); 298 | digitalWrite(PIN_LCD_BACKLIGHT, HIGH); 299 | state = State::PLAY_GIF; 300 | break; 301 | case State::PLAY_GIF: { 302 | if (right_button) { 303 | GifPlayer::stop(); 304 | int center = tft_.width()/2; 305 | tft_.fillScreen(TFT_BLACK); 306 | tft_.setTextSize(2); 307 | tft_.setTextDatum(TC_DATUM); 308 | tft_.drawString("Merry Christmas!", center, 10); 309 | tft_.setTextSize(1); 310 | tft_.drawString("Designed and handmade", center, 50); 311 | tft_.drawString("by Scott Bezek", center, 60); 312 | tft_.drawString("Oakland, 2021", center, 80); 313 | 314 | if (WiFi.status() == WL_CONNECTED) { 315 | tft_.setTextDatum(BL_DATUM); 316 | tft_.drawString(String("IP: ") + WiFi.localIP().toString(), 5, tft_.height()); 317 | } 318 | main_task_.setOtaEnabled(true); 319 | delay(200); 320 | state = State::SHOW_CREDITS; 321 | break; 322 | } 323 | bool is_christmas = isChristmas(); 324 | bool christmas_changed = false; 325 | if (is_christmas != last_christmas) { 326 | last_christmas = is_christmas; 327 | christmas_changed = true; 328 | } 329 | 330 | if (left_button || christmas_changed) { 331 | // Force select new gif, even if we hadn't met the minimum loop duration yet 332 | minimum_loop_duration = 0; 333 | GifPlayer::stop(); 334 | state = State::CHOOSE_GIF; 335 | break; 336 | } 337 | uint32_t time_since_last_frame = millis() - last_frame; 338 | if (time_since_last_frame > frame_delay) { 339 | // Time for the next frame; play it 340 | last_frame = millis(); 341 | if (!GifPlayer::play_frame(&frame_delay)) { 342 | GifPlayer::stop(); 343 | state = State::CHOOSE_GIF; 344 | break; 345 | } 346 | } else { 347 | // Wait until it's time for the next frame, but up to 50ms max at a time to avoid stalling UI thread 348 | delay(min((uint32_t)50, frame_delay - time_since_last_frame)); 349 | } 350 | 351 | break; 352 | } 353 | case State::SHOW_CREDITS: 354 | if (right_button) { 355 | // Exit credits 356 | main_task_.setOtaEnabled(false); 357 | state = State::CHOOSE_GIF; 358 | tft_.fillScreen(TFT_BLACK); 359 | delay(200); 360 | } 361 | break; 362 | } 363 | } 364 | } 365 | 366 | bool DisplayTask::isChristmas() { 367 | tm local; 368 | return main_task_.getLocalTime(&local) && local.tm_mon == 11 && local.tm_mday == 25; 369 | } 370 | 371 | void DisplayTask::handleLogRendering() { 372 | uint32_t now = millis(); 373 | // Check for new message 374 | bool force_redraw = false; 375 | if (now - last_message_millis_ > 100) { 376 | std::string* log_string; 377 | if (xQueueReceive(log_queue_, &log_string, 0) == pdTRUE) { 378 | last_message_millis_ = now; 379 | force_redraw = true; 380 | strncpy(current_message_, log_string->c_str(), sizeof(current_message_)); 381 | delete log_string; 382 | } 383 | } 384 | 385 | bool show = show_log_ && (now - last_message_millis_ < 3000); 386 | 387 | if (show && (!message_visible_ || force_redraw)) { 388 | GifPlayer::set_max_line(124); 389 | tft_.fillRect(0, 124, DISPLAY_WIDTH, 11, TFT_BLACK); 390 | tft_.setTextSize(1); 391 | tft_.setTextDatum(TL_DATUM); 392 | tft_.drawString(current_message_, 3, 126); 393 | } else if (!show && message_visible_) { 394 | tft_.fillRect(0, 124, DISPLAY_WIDTH, 11, TFT_BLACK); 395 | GifPlayer::set_max_line(-1); 396 | } 397 | message_visible_ = show; 398 | } 399 | 400 | void DisplayTask::log(const char* msg) { 401 | Serial.println(msg); 402 | // Allocate a string for the duration it's in the queue; it is free'd by the queue consumer 403 | std::string* msg_str = new std::string(msg); 404 | 405 | // Put string in queue (or drop if full to avoid blocking) 406 | if (xQueueSendToBack(log_queue_, &msg_str, 0) != pdTRUE) { 407 | delete msg_str; 408 | } 409 | } 410 | 411 | void DisplayTask::log(String msg) { 412 | log(msg.c_str()); 413 | } 414 | -------------------------------------------------------------------------------- /src/display_task.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "logger.h" 8 | #include "main_task.h" 9 | #include "task.h" 10 | 11 | enum class State { 12 | CHOOSE_GIF, 13 | PLAY_GIF, 14 | SHOW_CREDITS, 15 | }; 16 | 17 | class DisplayTask : public Task, public Logger { 18 | friend class Task; // Allow base Task to invoke protected run() 19 | 20 | public: 21 | DisplayTask(MainTask& main_task, const uint8_t task_core); 22 | virtual ~DisplayTask() {}; 23 | 24 | void log(const char* msg) override; 25 | 26 | protected: 27 | void run(); 28 | 29 | private: 30 | bool performUpdate(Stream &updateSource, size_t updateSize); 31 | bool updateFromFS(fs::FS &fs); 32 | int enumerateGifs( const char* basePath, std::vector& out_files); 33 | bool isChristmas(); 34 | void handleLogRendering(); 35 | 36 | void log(String msg); 37 | 38 | TFT_eSPI tft_ = TFT_eSPI(); 39 | MainTask& main_task_; 40 | QueueHandle_t log_queue_; 41 | QueueHandle_t event_queue_; 42 | 43 | bool show_log_ = false; 44 | bool message_visible_ = false; 45 | char current_message_[200]; 46 | uint32_t last_message_millis_ = UINT32_MAX; 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /src/event.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define BUTTON_ID_LEFT 0 6 | #define BUTTON_ID_RIGHT 1 7 | 8 | enum class EventType { 9 | BUTTON, 10 | }; 11 | 12 | struct EventButton { 13 | uint8_t button_id; 14 | uint8_t event; 15 | }; 16 | 17 | struct Event { 18 | EventType type; 19 | union { 20 | EventButton button; 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/gif_player.cpp: -------------------------------------------------------------------------------- 1 | #include "gif_player.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | AnimatedGIF GifPlayer::gif; 8 | TFT_eSPI* GifPlayer::tft; 9 | 10 | File GifPlayer::FSGifFile; // temp gif file holder 11 | 12 | #ifdef USE_DMA 13 | uint16_t GifPlayer::usTemp[2][BUFFER_SIZE]; // Global to support DMA use 14 | #else 15 | uint16_t GifPlayer::usTemp[1][BUFFER_SIZE]; // Global to support DMA use 16 | #endif 17 | bool GifPlayer::dmaBuf; 18 | 19 | int GifPlayer::frame_delay; 20 | int GifPlayer::max_line = -1; 21 | 22 | 23 | void * GifPlayer::GIFOpenFile(const char *fname, int32_t *pSize) 24 | { 25 | //log_d("GIFOpenFile( %s )\n", fname ); 26 | FSGifFile = SD_MMC.open(fname); 27 | if (FSGifFile) { 28 | *pSize = FSGifFile.size(); 29 | return (void *)&FSGifFile; 30 | } 31 | return NULL; 32 | } 33 | 34 | 35 | void GifPlayer::GIFCloseFile(void *pHandle) 36 | { 37 | File *f = static_cast(pHandle); 38 | if (f != NULL) 39 | f->close(); 40 | } 41 | 42 | 43 | int32_t GifPlayer::GIFReadFile(GIFFILE *pFile, uint8_t *pBuf, int32_t iLen) 44 | { 45 | int32_t iBytesRead; 46 | iBytesRead = iLen; 47 | File *f = static_cast(pFile->fHandle); 48 | // Note: If you read a file all the way to the last byte, seek() stops working 49 | if ((pFile->iSize - pFile->iPos) < iLen) 50 | iBytesRead = pFile->iSize - pFile->iPos - 1; // <-- ugly work-around 51 | if (iBytesRead <= 0) 52 | return 0; 53 | iBytesRead = (int32_t)f->read(pBuf, iBytesRead); 54 | pFile->iPos = f->position(); 55 | return iBytesRead; 56 | } 57 | 58 | 59 | int32_t GifPlayer::GIFSeekFile(GIFFILE *pFile, int32_t iPosition) 60 | { 61 | int i = micros(); 62 | File *f = static_cast(pFile->fHandle); 63 | f->seek(iPosition); 64 | pFile->iPos = (int32_t)f->position(); 65 | i = micros() - i; 66 | //log_d("Seek time = %d us\n", i); 67 | return pFile->iPos; 68 | } 69 | 70 | 71 | 72 | 73 | // From AnimatedGIF TFT_eSPI_memory example 74 | 75 | // Draw a line of image directly on the LCD 76 | void GifPlayer::GIFDraw(GIFDRAW *pDraw) 77 | { 78 | uint8_t *s; 79 | uint16_t *d, *usPalette; 80 | int x, y, iWidth, iCount; 81 | 82 | // Displ;ay bounds chech and cropping 83 | iWidth = pDraw->iWidth; 84 | if (iWidth + pDraw->iX > DISPLAY_WIDTH) 85 | iWidth = DISPLAY_WIDTH - pDraw->iX; 86 | usPalette = pDraw->pPalette; 87 | y = pDraw->iY + pDraw->y; // current line 88 | if (y >= DISPLAY_HEIGHT || pDraw->iX >= DISPLAY_WIDTH || iWidth < 1 || (max_line > -1 && y > max_line)) 89 | return; 90 | 91 | // Old image disposal 92 | s = pDraw->pPixels; 93 | if (pDraw->ucDisposalMethod == 2) // restore to background color 94 | { 95 | for (x = 0; x < iWidth; x++) 96 | { 97 | if (s[x] == pDraw->ucTransparent) 98 | s[x] = pDraw->ucBackground; 99 | } 100 | pDraw->ucHasTransparency = 0; 101 | } 102 | 103 | // Apply the new pixels to the main image 104 | if (pDraw->ucHasTransparency) // if transparency used 105 | { 106 | uint8_t *pEnd, c, ucTransparent = pDraw->ucTransparent; 107 | pEnd = s + iWidth; 108 | x = 0; 109 | iCount = 0; // count non-transparent pixels 110 | while (x < iWidth) 111 | { 112 | c = ucTransparent - 1; 113 | d = &usTemp[0][0]; 114 | while (c != ucTransparent && s < pEnd && iCount < BUFFER_SIZE ) 115 | { 116 | c = *s++; 117 | if (c == ucTransparent) // done, stop 118 | { 119 | s--; // back up to treat it like transparent 120 | } 121 | else // opaque 122 | { 123 | *d++ = usPalette[c]; 124 | iCount++; 125 | } 126 | } // while looking for opaque pixels 127 | if (iCount) // any opaque pixels? 128 | { 129 | // DMA would degrtade performance here due to short line segments 130 | tft->setAddrWindow(pDraw->iX + x, y, iCount, 1); 131 | tft->pushPixels(usTemp, iCount); 132 | x += iCount; 133 | iCount = 0; 134 | } 135 | // no, look for a run of transparent pixels 136 | c = ucTransparent; 137 | while (c == ucTransparent && s < pEnd) 138 | { 139 | c = *s++; 140 | if (c == ucTransparent) 141 | x++; 142 | else 143 | s--; 144 | } 145 | } 146 | } 147 | else 148 | { 149 | s = pDraw->pPixels; 150 | 151 | // Unroll the first pass to boost DMA performance 152 | // Translate the 8-bit pixels through the RGB565 palette (already byte reversed) 153 | if (iWidth <= BUFFER_SIZE) 154 | for (iCount = 0; iCount < iWidth; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++]; 155 | else 156 | for (iCount = 0; iCount < BUFFER_SIZE; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++]; 157 | 158 | #ifdef USE_DMA // 71.6 fps (ST7796 84.5 fps) 159 | tft->dmaWait(); 160 | tft->setAddrWindow(pDraw->iX, y, iWidth, 1); 161 | tft->pushPixelsDMA(&usTemp[dmaBuf][0], iCount); 162 | dmaBuf = !dmaBuf; 163 | #else // 57.0 fps 164 | tft->setAddrWindow(pDraw->iX, y, iWidth, 1); 165 | tft->pushPixels(&usTemp[0][0], iCount); 166 | #endif 167 | 168 | iWidth -= iCount; 169 | // Loop if pixel buffer smaller than width 170 | while (iWidth > 0) 171 | { 172 | // Translate the 8-bit pixels through the RGB565 palette (already byte reversed) 173 | if (iWidth <= BUFFER_SIZE) 174 | for (iCount = 0; iCount < iWidth; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++]; 175 | else 176 | for (iCount = 0; iCount < BUFFER_SIZE; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++]; 177 | 178 | #ifdef USE_DMA 179 | tft->dmaWait(); 180 | tft->pushPixelsDMA(&usTemp[dmaBuf][0], iCount); 181 | dmaBuf = !dmaBuf; 182 | #else 183 | tft->pushPixels(&usTemp[0][0], iCount); 184 | #endif 185 | iWidth -= iCount; 186 | } 187 | } 188 | } /* GIFDraw() */ 189 | 190 | 191 | 192 | 193 | bool GifPlayer::start(const char* path) { 194 | gif.begin(BIG_ENDIAN_PIXELS); 195 | 196 | if( ! gif.open( path, GIFOpenFile, GIFCloseFile, GIFReadFile, GIFSeekFile, GIFDraw ) ) { 197 | log_n("Could not open gif %s", path ); 198 | return false; 199 | } 200 | 201 | tft->startWrite(); 202 | return true; 203 | } 204 | 205 | bool GifPlayer::play_frame(int* frame_delay) { 206 | bool sync = frame_delay == nullptr; 207 | return gif.playFrame(sync, frame_delay) == 1; 208 | 209 | } 210 | 211 | void GifPlayer::stop() { 212 | gif.close(); 213 | tft->endWrite(); 214 | gif.reset(); 215 | } 216 | 217 | void GifPlayer::begin(TFT_eSPI* tft) { 218 | GifPlayer::tft = tft; 219 | 220 | dmaBuf = 0; 221 | } 222 | 223 | void GifPlayer::set_max_line(int l) { 224 | max_line = l; 225 | } 226 | -------------------------------------------------------------------------------- /src/gif_player.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #define DISPLAY_WIDTH 240 9 | #define DISPLAY_HEIGHT 135 10 | // #define USE_DMA 1 11 | #define BUFFER_SIZE 256 // Optimum is >= GIF width or integral division of width 12 | 13 | class GifPlayer { 14 | private: 15 | static AnimatedGIF gif; 16 | static TFT_eSPI* tft; 17 | 18 | static File FSGifFile; // temp gif file holder 19 | 20 | #ifdef USE_DMA 21 | static uint16_t usTemp[2][BUFFER_SIZE]; // Global to support DMA use 22 | #else 23 | static uint16_t usTemp[1][BUFFER_SIZE]; // Global to support DMA use 24 | #endif 25 | static bool dmaBuf; 26 | 27 | static int frame_delay; 28 | static int max_line; 29 | 30 | static void * GIFOpenFile(const char *fname, int32_t *pSize); 31 | static void GIFCloseFile(void *pHandle); 32 | static int32_t GIFReadFile(GIFFILE *pFile, uint8_t *pBuf, int32_t iLen); 33 | static int32_t GIFSeekFile(GIFFILE *pFile, int32_t iPosition); 34 | static void GIFDraw(GIFDRAW *pDraw); 35 | 36 | public: 37 | static void begin(TFT_eSPI* tft); 38 | 39 | static bool start(const char* path); 40 | static bool play_frame(int* frame_delay); 41 | static void stop(); 42 | 43 | static void set_max_line(int l); 44 | 45 | }; 46 | -------------------------------------------------------------------------------- /src/logger.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class Logger { 4 | public: 5 | Logger() {}; 6 | virtual ~Logger() {}; 7 | virtual void log(const char* msg) = 0; 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "display_task.h" 4 | #include "main_task.h" 5 | 6 | MainTask main_task = MainTask(0); 7 | DisplayTask display_task = DisplayTask(main_task, 1); 8 | 9 | void setup() { 10 | Serial.begin(921600); 11 | 12 | main_task.begin(); 13 | display_task.begin(); 14 | 15 | vTaskDelete(NULL); 16 | } 17 | 18 | 19 | void loop() { 20 | assert(false); 21 | } -------------------------------------------------------------------------------- /src/main_task.cpp: -------------------------------------------------------------------------------- 1 | #include "main_task.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "semaphore_guard.h" 10 | 11 | #define TASK_NOTIFY_SET_CONFIG (1 << 0) 12 | 13 | #define MDNS_NAME "switchOrnament" 14 | #define OTA_PASSWORD "hunter2" 15 | 16 | #define PIN_LEFT_BUTTON 32 17 | #define PIN_RIGHT_BUTTON 26 18 | 19 | using namespace ace_button; 20 | 21 | MainTask::MainTask(const uint8_t task_core) : Task{"Main", 8192, 1, task_core}, semaphore_(xSemaphoreCreateMutex()) { 22 | assert(semaphore_ != NULL); 23 | xSemaphoreGive(semaphore_); 24 | } 25 | 26 | MainTask::~MainTask() { 27 | if (semaphore_ != NULL) { 28 | vSemaphoreDelete(semaphore_); 29 | } 30 | } 31 | 32 | void MainTask::run() { 33 | WiFi.mode(WIFI_STA); 34 | 35 | AceButton left_button(PIN_LEFT_BUTTON, 1, BUTTON_ID_LEFT); 36 | AceButton right_button(PIN_RIGHT_BUTTON, 1, BUTTON_ID_RIGHT); 37 | 38 | pinMode(PIN_LEFT_BUTTON, INPUT_PULLUP); 39 | pinMode(PIN_RIGHT_BUTTON, INPUT_PULLUP); 40 | 41 | ButtonConfig* config = ButtonConfig::getSystemButtonConfig(); 42 | config->setIEventHandler(this); 43 | 44 | ArduinoOTA 45 | .onStart([this]() { 46 | String type; 47 | if (ArduinoOTA.getCommand() == U_FLASH) 48 | type = "(flash)"; 49 | else // U_SPIFFS 50 | type = "(filesystem)"; 51 | 52 | // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() 53 | log("Start OTA " + type); 54 | }) 55 | .onEnd([this]() { 56 | log("OTA End"); 57 | }) 58 | .onProgress([this](unsigned int progress, unsigned int total) { 59 | static uint32_t last_progress; 60 | if (millis() - last_progress > 1000) { 61 | log("OTA Progress: " + String((int)(progress * 100 / total)) + "%"); 62 | last_progress = millis(); 63 | } 64 | }) 65 | .onError([this](ota_error_t error) { 66 | log("Error[%u]: " + String(error)); 67 | if (error == OTA_AUTH_ERROR) log("Auth Failed"); 68 | else if (error == OTA_BEGIN_ERROR) log("Begin Failed"); 69 | else if (error == OTA_CONNECT_ERROR) log("Connect Failed"); 70 | else if (error == OTA_RECEIVE_ERROR) log("Receive Failed"); 71 | else if (error == OTA_END_ERROR) log("End Failed"); 72 | }); 73 | ArduinoOTA.setHostname(MDNS_NAME); 74 | ArduinoOTA.setPassword(OTA_PASSWORD); 75 | 76 | wl_status_t wifi_status = WL_DISCONNECTED; 77 | while (1) { 78 | uint32_t notify_value = 0; 79 | if (xTaskNotifyWait(0, ULONG_MAX, ¬ify_value, 0) == pdTRUE) { 80 | if (notify_value && TASK_NOTIFY_SET_CONFIG) { 81 | String wifi_ssid, wifi_password, timezone; 82 | { 83 | SemaphoreGuard lock(semaphore_); 84 | wifi_ssid = wifi_ssid_; 85 | wifi_password = wifi_password_; 86 | timezone = timezone_; 87 | } 88 | setenv("TZ", timezone.c_str(), 1); 89 | tzset(); 90 | 91 | char buf[200]; 92 | snprintf(buf, sizeof(buf), "Connecting to %s...", wifi_ssid.c_str()); 93 | log(buf); 94 | WiFi.begin(wifi_ssid.c_str(), wifi_password.c_str()); 95 | } 96 | } 97 | 98 | wl_status_t new_status = WiFi.status(); 99 | if (new_status != wifi_status) { 100 | char buf[200]; 101 | snprintf(buf, sizeof(buf), "Wifi status changed to %d\n", new_status); 102 | log(buf); 103 | if (new_status == WL_CONNECTED) { 104 | snprintf(buf, sizeof(buf), "IP: %s", WiFi.localIP().toString().c_str()); 105 | log(buf); 106 | 107 | delay(100); 108 | // Sync SNTP 109 | sntp_setoperatingmode(SNTP_OPMODE_POLL); 110 | 111 | char server[] = "time.nist.gov"; // sntp_setservername takes a non-const char*, so use a non-const variable to avoid warning 112 | sntp_setservername(0, server); 113 | sntp_init(); 114 | } 115 | 116 | wifi_status = new_status; 117 | } 118 | 119 | 120 | time_t now = 0; 121 | bool ntp_just_synced = false; 122 | { 123 | SemaphoreGuard lock(semaphore_); 124 | if (!ntp_synced_) { 125 | // Check if NTP has synced yet 126 | time(&now); 127 | if (now > 1625099485) { 128 | ntp_just_synced = true; 129 | ntp_synced_ = true; 130 | } 131 | } 132 | } 133 | 134 | if (ntp_just_synced) { 135 | // We do this separately from above to avoid deadlock: log() requires semaphore_ and we're non-reentrant-locking 136 | char buf[200]; 137 | strftime(buf, sizeof(buf), "Got time: %Y-%m-%d %H:%M:%S", localtime(&now)); 138 | Serial.printf("%s\n", buf); 139 | log(buf); 140 | } 141 | 142 | ArduinoOTA.handle(); 143 | left_button.check(); 144 | right_button.check(); 145 | delay(1); 146 | } 147 | } 148 | 149 | void MainTask::setConfig(const char* wifi_ssid, const char* wifi_password, const char* timezone) { 150 | { 151 | SemaphoreGuard lock(semaphore_); 152 | wifi_ssid_ = String(wifi_ssid); 153 | wifi_password_ = String(wifi_password); 154 | timezone_ = String(timezone); 155 | } 156 | xTaskNotify(getHandle(), TASK_NOTIFY_SET_CONFIG, eSetBits); 157 | } 158 | 159 | bool MainTask::getLocalTime(tm* t) { 160 | SemaphoreGuard lock(semaphore_); 161 | if (!ntp_synced_) { 162 | return false; 163 | } 164 | time_t now = 0; 165 | time(&now); 166 | localtime_r(&now, t); 167 | return true; 168 | } 169 | 170 | void MainTask::setLogger(Logger* logger) { 171 | SemaphoreGuard lock(semaphore_); 172 | logger_ = logger; 173 | } 174 | 175 | void MainTask::setOtaEnabled(bool enabled) { 176 | if (enabled) { 177 | ArduinoOTA.begin(); 178 | } else { 179 | ArduinoOTA.end(); 180 | } 181 | } 182 | 183 | void MainTask::log(const char* message) { 184 | SemaphoreGuard lock(semaphore_); 185 | if (logger_ != nullptr) { 186 | logger_->log(message); 187 | } else { 188 | Serial.println(message); 189 | } 190 | } 191 | 192 | void MainTask::log(String message) { 193 | log(message.c_str()); 194 | } 195 | 196 | void MainTask::registerEventQueue(QueueHandle_t queue) { 197 | SemaphoreGuard lock(semaphore_); 198 | event_queues_.push_back(queue); 199 | } 200 | 201 | void MainTask::publishEvent(Event event) { 202 | SemaphoreGuard lock(semaphore_); 203 | for (QueueHandle_t queue : event_queues_) { 204 | xQueueSend(queue, &event, 0); 205 | } 206 | } 207 | 208 | void MainTask::handleEvent(AceButton* button, uint8_t event_type, uint8_t button_state) { 209 | Event event = { 210 | .type = EventType::BUTTON, 211 | { 212 | .button = { 213 | .button_id = button->getId(), 214 | .event = event_type, 215 | }, 216 | } 217 | }; 218 | publishEvent(event); 219 | } 220 | -------------------------------------------------------------------------------- /src/main_task.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "event.h" 8 | #include "logger.h" 9 | #include "task.h" 10 | 11 | class MainTask : public Task, public ace_button::IEventHandler { 12 | friend class Task; // Allow base Task to invoke protected run() 13 | 14 | public: 15 | MainTask(const uint8_t task_core); 16 | virtual ~MainTask(); 17 | 18 | void setConfig(const char* wifi_ssid, const char* wifi_password, const char* timezone); 19 | bool getLocalTime(tm* t); 20 | void setLogger(Logger* logger); 21 | void setOtaEnabled(bool enabled); 22 | void registerEventQueue(QueueHandle_t queue); 23 | 24 | void handleEvent(ace_button::AceButton* button, uint8_t event_type, uint8_t button_state) override; 25 | 26 | protected: 27 | void run(); 28 | 29 | private: 30 | 31 | void log(const char* message); 32 | void log(String message); 33 | 34 | void publishEvent(Event event); 35 | 36 | SemaphoreHandle_t semaphore_; 37 | 38 | String wifi_ssid_; 39 | String wifi_password_; 40 | String timezone_; 41 | 42 | bool ntp_synced_ = false; 43 | 44 | Logger* logger_ = nullptr; 45 | 46 | std::vector event_queues_; 47 | }; 48 | -------------------------------------------------------------------------------- /src/semaphore_guard.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Scott Bezek and the splitflap contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #pragma once 17 | 18 | #include 19 | 20 | class SemaphoreGuard { 21 | public: 22 | SemaphoreGuard(SemaphoreHandle_t handle) : handle_{handle} { 23 | xSemaphoreTake(handle_, portMAX_DELAY); 24 | } 25 | ~SemaphoreGuard() { 26 | xSemaphoreGive(handle_); 27 | } 28 | SemaphoreGuard(SemaphoreGuard const&)=delete; 29 | SemaphoreGuard& operator=(SemaphoreGuard const&)=delete; 30 | 31 | private: 32 | SemaphoreHandle_t handle_; 33 | }; 34 | -------------------------------------------------------------------------------- /src/task.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Scott Bezek and the splitflap contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | #pragma once 17 | 18 | #include 19 | 20 | // Static polymorphic abstract base class for a FreeRTOS task using CRTP pattern. Concrete implementations 21 | // should implement a run() method. 22 | // Inspired by https://fjrg76.wordpress.com/2018/05/23/objectifying-task-creation-in-freertos-ii/ 23 | template 24 | class Task { 25 | public: 26 | Task(const char* name, uint32_t stackDepth, UBaseType_t priority, const BaseType_t coreId = tskNO_AFFINITY) : 27 | name { name }, 28 | stackDepth {stackDepth}, 29 | priority { priority }, 30 | coreId { coreId } 31 | {} 32 | virtual ~Task() {}; 33 | 34 | TaskHandle_t getHandle() { 35 | return taskHandle; 36 | } 37 | 38 | void begin() { 39 | BaseType_t result = xTaskCreatePinnedToCore(taskFunction, name, stackDepth, this, priority, &taskHandle, coreId); 40 | assert("Failed to create task" && result == pdPASS); 41 | } 42 | 43 | private: 44 | static void taskFunction(void* params) { 45 | T* t = static_cast(params); 46 | t->run(); 47 | } 48 | 49 | const char* name; 50 | uint32_t stackDepth; 51 | UBaseType_t priority; 52 | TaskHandle_t taskHandle; 53 | const BaseType_t coreId; 54 | }; 55 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PIO Unit Testing and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PIO Unit Testing: 11 | - https://docs.platformio.org/page/plus/unit-testing.html 12 | --------------------------------------------------------------------------------