diff --git a/.DS_Store b/.DS_Store index c3f80ec..4f7147e 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Firmware/.gitignore b/Firmware/.gitignore new file mode 100644 index 0000000..f816df4 --- /dev/null +++ b/Firmware/.gitignore @@ -0,0 +1,20 @@ +.pio +.pioenvs +.piolibdeps +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/settings.json + +# KiCAD files +*.000 +*.bak +*.bck +*.kicad_pcb-bak +*.sch-bak +*.net +*.dsn +fp-info-cache + +src/boot_gif.h +secrets.h \ No newline at end of file diff --git a/Firmware/LICENSE b/Firmware/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/Firmware/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Firmware/README.md b/Firmware/README.md new file mode 100644 index 0000000..0b23733 --- /dev/null +++ b/Firmware/README.md @@ -0,0 +1,7 @@ +Reference code for my homemade Nintendo Switch ornament: + + + +Plays animated gifs from the SD card using the `bitbank2/AnimatedGIF` library and `TFT_eSPI` display driver. + +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. diff --git a/Firmware/data/config.json b/Firmware/data/config.json new file mode 100644 index 0000000..51d4f69 --- /dev/null +++ b/Firmware/data/config.json @@ -0,0 +1,4 @@ +{"show_log": "true", +"ssid": "iot", +"password": "Rijnstraat214", +"timezone": "CET "} \ No newline at end of file diff --git a/Firmware/include/README b/Firmware/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/Firmware/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/Firmware/lib/json11/CMakeLists.txt b/Firmware/lib/json11/CMakeLists.txt new file mode 100644 index 0000000..64116d6 --- /dev/null +++ b/Firmware/lib/json11/CMakeLists.txt @@ -0,0 +1,58 @@ +cmake_minimum_required(VERSION 2.8) +if (CMAKE_VERSION VERSION_LESS "3") + project(json11 CXX) +else() + cmake_policy(SET CMP0048 NEW) + project(json11 VERSION 1.0.0 LANGUAGES CXX) +endif() + +enable_testing() + +option(JSON11_BUILD_TESTS "Build unit tests" OFF) +option(JSON11_ENABLE_DR1467_CANARY "Enable canary test for DR 1467" OFF) + +if(CMAKE_VERSION VERSION_LESS "3") + add_definitions(-std=c++11) +else() + set(CMAKE_CXX_STANDARD 11) + set(CMAKE_CXX_STANDARD_REQUIRED ON) +endif() + +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX /usr) +endif() + +add_library(json11 json11.cpp) +target_include_directories(json11 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_options(json11 + PRIVATE -fPIC -fno-rtti -fno-exceptions -Wall) + +# Set warning flags, which may vary per platform +include(CheckCXXCompilerFlag) +set(_possible_warnings_flags /W4 /WX -Wextra -Werror) +foreach(_warning_flag ${_possible_warnings_flags}) + unset(_flag_supported) + CHECK_CXX_COMPILER_FLAG(${_warning_flag} _flag_supported) + if(${_flag_supported}) + target_compile_options(json11 PRIVATE ${_warning_flag}) + endif() +endforeach() + +configure_file("json11.pc.in" "json11.pc" @ONLY) + +if (JSON11_BUILD_TESTS) + + # enable test for DR1467, described here: https://llvm.org/bugs/show_bug.cgi?id=23812 + if(JSON11_ENABLE_DR1467_CANARY) + add_definitions(-D JSON11_ENABLE_DR1467_CANARY=1) + else() + add_definitions(-D JSON11_ENABLE_DR1467_CANARY=0) + endif() + + add_executable(json11_test test.cpp) + target_link_libraries(json11_test json11) +endif() + +install(TARGETS json11 DESTINATION lib/${CMAKE_LIBRARY_ARCHITECTURE}) +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/json11.hpp" DESTINATION include/${CMAKE_LIBRARY_ARCHITECTURE}) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/json11.pc" DESTINATION lib/${CMAKE_LIBRARY_ARCHITECTURE}/pkgconfig) diff --git a/Firmware/lib/json11/LICENSE.txt b/Firmware/lib/json11/LICENSE.txt new file mode 100644 index 0000000..691742e --- /dev/null +++ b/Firmware/lib/json11/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2013 Dropbox, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Firmware/lib/json11/Makefile b/Firmware/lib/json11/Makefile new file mode 100644 index 0000000..f946bdd --- /dev/null +++ b/Firmware/lib/json11/Makefile @@ -0,0 +1,15 @@ +# Environment variable to enable or disable code which demonstrates the behavior change +# in Xcode 7 / Clang 3.7, introduced by DR1467 and described here: +# https://llvm.org/bugs/show_bug.cgi?id=23812 +# Defaults to on in order to act as a warning to anyone who's unaware of the issue. +ifneq ($(JSON11_ENABLE_DR1467_CANARY),) +CANARY_ARGS = -DJSON11_ENABLE_DR1467_CANARY=$(JSON11_ENABLE_DR1467_CANARY) +endif + +test: json11.cpp json11.hpp test.cpp + $(CXX) $(CANARY_ARGS) -O -std=c++11 json11.cpp test.cpp -o test -fno-rtti -fno-exceptions + +clean: + if [ -e test ]; then rm test; fi + +.PHONY: clean diff --git a/Firmware/lib/json11/README.md b/Firmware/lib/json11/README.md new file mode 100644 index 0000000..596dfaf --- /dev/null +++ b/Firmware/lib/json11/README.md @@ -0,0 +1,42 @@ +json11 +------ + +json11 is a tiny JSON library for C++11, providing JSON parsing and serialization. + +The core object provided by the library is json11::Json. A Json object represents any JSON +value: null, bool, number (int or double), string (std::string), array (std::vector), or +object (std::map). + +Json objects act like values. They can be assigned, copied, moved, compared for equality or +order, and so on. There are also helper methods Json::dump, to serialize a Json to a string, and +Json::parse (static) to parse a std::string as a Json object. + +It's easy to make a JSON object with C++11's new initializer syntax: + + Json my_json = Json::object { + { "key1", "value1" }, + { "key2", false }, + { "key3", Json::array { 1, 2, 3 } }, + }; + std::string json_str = my_json.dump(); + +There are also implicit constructors that allow standard and user-defined types to be +automatically converted to JSON. For example: + + class Point { + public: + int x; + int y; + Point (int x, int y) : x(x), y(y) {} + Json to_json() const { return Json::array { x, y }; } + }; + + std::vector points = { { 1, 2 }, { 10, 20 }, { 100, 200 } }; + std::string points_json = Json(points).dump(); + +JSON values can have their values queried and inspected: + + Json json = Json::array { Json::object { { "k", "v" } } }; + std::string str = json[0]["k"].string_value(); + +For more documentation see json11.hpp. diff --git a/Firmware/lib/json11/json11.cpp b/Firmware/lib/json11/json11.cpp new file mode 100644 index 0000000..88024e9 --- /dev/null +++ b/Firmware/lib/json11/json11.cpp @@ -0,0 +1,790 @@ +/* Copyright (c) 2013 Dropbox, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "json11.hpp" +#include +#include +#include +#include +#include + +namespace json11 { + +static const int max_depth = 200; + +using std::string; +using std::vector; +using std::map; +using std::make_shared; +using std::initializer_list; +using std::move; + +/* Helper for representing null - just a do-nothing struct, plus comparison + * operators so the helpers in JsonValue work. We can't use nullptr_t because + * it may not be orderable. + */ +struct NullStruct { + bool operator==(NullStruct) const { return true; } + bool operator<(NullStruct) const { return false; } +}; + +/* * * * * * * * * * * * * * * * * * * * + * Serialization + */ + +static void dump(NullStruct, string &out) { + out += "null"; +} + +static void dump(double value, string &out) { + if (std::isfinite(value)) { + char buf[32]; + snprintf(buf, sizeof buf, "%.17g", value); + out += buf; + } else { + out += "null"; + } +} + +static void dump(int value, string &out) { + char buf[32]; + snprintf(buf, sizeof buf, "%d", value); + out += buf; +} + +static void dump(bool value, string &out) { + out += value ? "true" : "false"; +} + +static void dump(const string &value, string &out) { + out += '"'; + for (size_t i = 0; i < value.length(); i++) { + const char ch = value[i]; + if (ch == '\\') { + out += "\\\\"; + } else if (ch == '"') { + out += "\\\""; + } else if (ch == '\b') { + out += "\\b"; + } else if (ch == '\f') { + out += "\\f"; + } else if (ch == '\n') { + out += "\\n"; + } else if (ch == '\r') { + out += "\\r"; + } else if (ch == '\t') { + out += "\\t"; + } else if (static_cast(ch) <= 0x1f) { + char buf[8]; + snprintf(buf, sizeof buf, "\\u%04x", ch); + out += buf; + } else if (static_cast(ch) == 0xe2 && static_cast(value[i+1]) == 0x80 + && static_cast(value[i+2]) == 0xa8) { + out += "\\u2028"; + i += 2; + } else if (static_cast(ch) == 0xe2 && static_cast(value[i+1]) == 0x80 + && static_cast(value[i+2]) == 0xa9) { + out += "\\u2029"; + i += 2; + } else { + out += ch; + } + } + out += '"'; +} + +static void dump(const Json::array &values, string &out) { + bool first = true; + out += "["; + for (const auto &value : values) { + if (!first) + out += ", "; + value.dump(out); + first = false; + } + out += "]"; +} + +static void dump(const Json::object &values, string &out) { + bool first = true; + out += "{"; + for (const auto &kv : values) { + if (!first) + out += ", "; + dump(kv.first, out); + out += ": "; + kv.second.dump(out); + first = false; + } + out += "}"; +} + +void Json::dump(string &out) const { + m_ptr->dump(out); +} + +/* * * * * * * * * * * * * * * * * * * * + * Value wrappers + */ + +template +class Value : public JsonValue { +protected: + + // Constructors + explicit Value(const T &value) : m_value(value) {} + explicit Value(T &&value) : m_value(move(value)) {} + + // Get type tag + Json::Type type() const override { + return tag; + } + + // Comparisons + bool equals(const JsonValue * other) const override { + return m_value == static_cast *>(other)->m_value; + } + bool less(const JsonValue * other) const override { + return m_value < static_cast *>(other)->m_value; + } + + const T m_value; + void dump(string &out) const override { json11::dump(m_value, out); } +}; + +class JsonDouble final : public Value { + double number_value() const override { return m_value; } + int int_value() const override { return static_cast(m_value); } + bool equals(const JsonValue * other) const override { return m_value == other->number_value(); } + bool less(const JsonValue * other) const override { return m_value < other->number_value(); } +public: + explicit JsonDouble(double value) : Value(value) {} +}; + +class JsonInt final : public Value { + double number_value() const override { return m_value; } + int int_value() const override { return m_value; } + bool equals(const JsonValue * other) const override { return m_value == other->number_value(); } + bool less(const JsonValue * other) const override { return m_value < other->number_value(); } +public: + explicit JsonInt(int value) : Value(value) {} +}; + +class JsonBoolean final : public Value { + bool bool_value() const override { return m_value; } +public: + explicit JsonBoolean(bool value) : Value(value) {} +}; + +class JsonString final : public Value { + const string &string_value() const override { return m_value; } +public: + explicit JsonString(const string &value) : Value(value) {} + explicit JsonString(string &&value) : Value(move(value)) {} +}; + +class JsonArray final : public Value { + const Json::array &array_items() const override { return m_value; } + const Json & operator[](size_t i) const override; +public: + explicit JsonArray(const Json::array &value) : Value(value) {} + explicit JsonArray(Json::array &&value) : Value(move(value)) {} +}; + +class JsonObject final : public Value { + const Json::object &object_items() const override { return m_value; } + const Json & operator[](const string &key) const override; +public: + explicit JsonObject(const Json::object &value) : Value(value) {} + explicit JsonObject(Json::object &&value) : Value(move(value)) {} +}; + +class JsonNull final : public Value { +public: + JsonNull() : Value({}) {} +}; + +/* * * * * * * * * * * * * * * * * * * * + * Static globals - static-init-safe + */ +struct Statics { + const std::shared_ptr null = make_shared(); + const std::shared_ptr t = make_shared(true); + const std::shared_ptr f = make_shared(false); + const string empty_string; + const vector empty_vector; + const map empty_map; + Statics() {} +}; + +static const Statics & statics() { + static const Statics s {}; + return s; +} + +static const Json & static_null() { + // This has to be separate, not in Statics, because Json() accesses statics().null. + static const Json json_null; + return json_null; +} + +/* * * * * * * * * * * * * * * * * * * * + * Constructors + */ + +Json::Json() noexcept : m_ptr(statics().null) {} +Json::Json(std::nullptr_t) noexcept : m_ptr(statics().null) {} +Json::Json(double value) : m_ptr(make_shared(value)) {} +Json::Json(int value) : m_ptr(make_shared(value)) {} +Json::Json(bool value) : m_ptr(value ? statics().t : statics().f) {} +Json::Json(const string &value) : m_ptr(make_shared(value)) {} +Json::Json(string &&value) : m_ptr(make_shared(move(value))) {} +Json::Json(const char * value) : m_ptr(make_shared(value)) {} +Json::Json(const Json::array &values) : m_ptr(make_shared(values)) {} +Json::Json(Json::array &&values) : m_ptr(make_shared(move(values))) {} +Json::Json(const Json::object &values) : m_ptr(make_shared(values)) {} +Json::Json(Json::object &&values) : m_ptr(make_shared(move(values))) {} + +/* * * * * * * * * * * * * * * * * * * * + * Accessors + */ + +Json::Type Json::type() const { return m_ptr->type(); } +double Json::number_value() const { return m_ptr->number_value(); } +int Json::int_value() const { return m_ptr->int_value(); } +bool Json::bool_value() const { return m_ptr->bool_value(); } +const string & Json::string_value() const { return m_ptr->string_value(); } +const vector & Json::array_items() const { return m_ptr->array_items(); } +const map & Json::object_items() const { return m_ptr->object_items(); } +const Json & Json::operator[] (size_t i) const { return (*m_ptr)[i]; } +const Json & Json::operator[] (const string &key) const { return (*m_ptr)[key]; } + +double JsonValue::number_value() const { return 0; } +int JsonValue::int_value() const { return 0; } +bool JsonValue::bool_value() const { return false; } +const string & JsonValue::string_value() const { return statics().empty_string; } +const vector & JsonValue::array_items() const { return statics().empty_vector; } +const map & JsonValue::object_items() const { return statics().empty_map; } +const Json & JsonValue::operator[] (size_t) const { return static_null(); } +const Json & JsonValue::operator[] (const string &) const { return static_null(); } + +const Json & JsonObject::operator[] (const string &key) const { + auto iter = m_value.find(key); + return (iter == m_value.end()) ? static_null() : iter->second; +} +const Json & JsonArray::operator[] (size_t i) const { + if (i >= m_value.size()) return static_null(); + else return m_value[i]; +} + +/* * * * * * * * * * * * * * * * * * * * + * Comparison + */ + +bool Json::operator== (const Json &other) const { + if (m_ptr == other.m_ptr) + return true; + if (m_ptr->type() != other.m_ptr->type()) + return false; + + return m_ptr->equals(other.m_ptr.get()); +} + +bool Json::operator< (const Json &other) const { + if (m_ptr == other.m_ptr) + return false; + if (m_ptr->type() != other.m_ptr->type()) + return m_ptr->type() < other.m_ptr->type(); + + return m_ptr->less(other.m_ptr.get()); +} + +/* * * * * * * * * * * * * * * * * * * * + * Parsing + */ + +/* esc(c) + * + * Format char c suitable for printing in an error message. + */ +static inline string esc(char c) { + char buf[12]; + if (static_cast(c) >= 0x20 && static_cast(c) <= 0x7f) { + snprintf(buf, sizeof buf, "'%c' (%d)", c, c); + } else { + snprintf(buf, sizeof buf, "(%d)", c); + } + return string(buf); +} + +static inline bool in_range(long x, long lower, long upper) { + return (x >= lower && x <= upper); +} + +namespace { +/* JsonParser + * + * Object that tracks all state of an in-progress parse. + */ +struct JsonParser final { + + /* State + */ + const string &str; + size_t i; + string &err; + bool failed; + const JsonParse strategy; + + /* fail(msg, err_ret = Json()) + * + * Mark this parse as failed. + */ + Json fail(string &&msg) { + return fail(move(msg), Json()); + } + + template + T fail(string &&msg, const T err_ret) { + if (!failed) + err = std::move(msg); + failed = true; + return err_ret; + } + + /* consume_whitespace() + * + * Advance until the current character is non-whitespace. + */ + void consume_whitespace() { + while (str[i] == ' ' || str[i] == '\r' || str[i] == '\n' || str[i] == '\t') + i++; + } + + /* consume_comment() + * + * Advance comments (c-style inline and multiline). + */ + bool consume_comment() { + bool comment_found = false; + if (str[i] == '/') { + i++; + if (i == str.size()) + return fail("unexpected end of input after start of comment", false); + if (str[i] == '/') { // inline comment + i++; + // advance until next line, or end of input + while (i < str.size() && str[i] != '\n') { + i++; + } + comment_found = true; + } + else if (str[i] == '*') { // multiline comment + i++; + if (i > str.size()-2) + return fail("unexpected end of input inside multi-line comment", false); + // advance until closing tokens + while (!(str[i] == '*' && str[i+1] == '/')) { + i++; + if (i > str.size()-2) + return fail( + "unexpected end of input inside multi-line comment", false); + } + i += 2; + comment_found = true; + } + else + return fail("malformed comment", false); + } + return comment_found; + } + + /* consume_garbage() + * + * Advance until the current character is non-whitespace and non-comment. + */ + void consume_garbage() { + consume_whitespace(); + if(strategy == JsonParse::COMMENTS) { + bool comment_found = false; + do { + comment_found = consume_comment(); + if (failed) return; + consume_whitespace(); + } + while(comment_found); + } + } + + /* get_next_token() + * + * Return the next non-whitespace character. If the end of the input is reached, + * flag an error and return 0. + */ + char get_next_token() { + consume_garbage(); + if (failed) return static_cast(0); + if (i == str.size()) + return fail("unexpected end of input", static_cast(0)); + + return str[i++]; + } + + /* encode_utf8(pt, out) + * + * Encode pt as UTF-8 and add it to out. + */ + void encode_utf8(long pt, string & out) { + if (pt < 0) + return; + + if (pt < 0x80) { + out += static_cast(pt); + } else if (pt < 0x800) { + out += static_cast((pt >> 6) | 0xC0); + out += static_cast((pt & 0x3F) | 0x80); + } else if (pt < 0x10000) { + out += static_cast((pt >> 12) | 0xE0); + out += static_cast(((pt >> 6) & 0x3F) | 0x80); + out += static_cast((pt & 0x3F) | 0x80); + } else { + out += static_cast((pt >> 18) | 0xF0); + out += static_cast(((pt >> 12) & 0x3F) | 0x80); + out += static_cast(((pt >> 6) & 0x3F) | 0x80); + out += static_cast((pt & 0x3F) | 0x80); + } + } + + /* parse_string() + * + * Parse a string, starting at the current position. + */ + string parse_string() { + string out; + long last_escaped_codepoint = -1; + while (true) { + if (i == str.size()) + return fail("unexpected end of input in string", ""); + + char ch = str[i++]; + + if (ch == '"') { + encode_utf8(last_escaped_codepoint, out); + return out; + } + + if (in_range(ch, 0, 0x1f)) + return fail("unescaped " + esc(ch) + " in string", ""); + + // The usual case: non-escaped characters + if (ch != '\\') { + encode_utf8(last_escaped_codepoint, out); + last_escaped_codepoint = -1; + out += ch; + continue; + } + + // Handle escapes + if (i == str.size()) + return fail("unexpected end of input in string", ""); + + ch = str[i++]; + + if (ch == 'u') { + // Extract 4-byte escape sequence + string esc = str.substr(i, 4); + // Explicitly check length of the substring. The following loop + // relies on std::string returning the terminating NUL when + // accessing str[length]. Checking here reduces brittleness. + if (esc.length() < 4) { + return fail("bad \\u escape: " + esc, ""); + } + for (size_t j = 0; j < 4; j++) { + if (!in_range(esc[j], 'a', 'f') && !in_range(esc[j], 'A', 'F') + && !in_range(esc[j], '0', '9')) + return fail("bad \\u escape: " + esc, ""); + } + + long codepoint = strtol(esc.data(), nullptr, 16); + + // JSON specifies that characters outside the BMP shall be encoded as a pair + // of 4-hex-digit \u escapes encoding their surrogate pair components. Check + // whether we're in the middle of such a beast: the previous codepoint was an + // escaped lead (high) surrogate, and this is a trail (low) surrogate. + if (in_range(last_escaped_codepoint, 0xD800, 0xDBFF) + && in_range(codepoint, 0xDC00, 0xDFFF)) { + // Reassemble the two surrogate pairs into one astral-plane character, per + // the UTF-16 algorithm. + encode_utf8((((last_escaped_codepoint - 0xD800) << 10) + | (codepoint - 0xDC00)) + 0x10000, out); + last_escaped_codepoint = -1; + } else { + encode_utf8(last_escaped_codepoint, out); + last_escaped_codepoint = codepoint; + } + + i += 4; + continue; + } + + encode_utf8(last_escaped_codepoint, out); + last_escaped_codepoint = -1; + + if (ch == 'b') { + out += '\b'; + } else if (ch == 'f') { + out += '\f'; + } else if (ch == 'n') { + out += '\n'; + } else if (ch == 'r') { + out += '\r'; + } else if (ch == 't') { + out += '\t'; + } else if (ch == '"' || ch == '\\' || ch == '/') { + out += ch; + } else { + return fail("invalid escape character " + esc(ch), ""); + } + } + } + + /* parse_number() + * + * Parse a double. + */ + Json parse_number() { + size_t start_pos = i; + + if (str[i] == '-') + i++; + + // Integer part + if (str[i] == '0') { + i++; + if (in_range(str[i], '0', '9')) + return fail("leading 0s not permitted in numbers"); + } else if (in_range(str[i], '1', '9')) { + i++; + while (in_range(str[i], '0', '9')) + i++; + } else { + return fail("invalid " + esc(str[i]) + " in number"); + } + + if (str[i] != '.' && str[i] != 'e' && str[i] != 'E' + && (i - start_pos) <= static_cast(std::numeric_limits::digits10)) { + return std::atoi(str.c_str() + start_pos); + } + + // Decimal part + if (str[i] == '.') { + i++; + if (!in_range(str[i], '0', '9')) + return fail("at least one digit required in fractional part"); + + while (in_range(str[i], '0', '9')) + i++; + } + + // Exponent part + if (str[i] == 'e' || str[i] == 'E') { + i++; + + if (str[i] == '+' || str[i] == '-') + i++; + + if (!in_range(str[i], '0', '9')) + return fail("at least one digit required in exponent"); + + while (in_range(str[i], '0', '9')) + i++; + } + + return std::strtod(str.c_str() + start_pos, nullptr); + } + + /* expect(str, res) + * + * Expect that 'str' starts at the character that was just read. If it does, advance + * the input and return res. If not, flag an error. + */ + Json expect(const string &expected, Json res) { + assert(i != 0); + i--; + if (str.compare(i, expected.length(), expected) == 0) { + i += expected.length(); + return res; + } else { + return fail("parse error: expected " + expected + ", got " + str.substr(i, expected.length())); + } + } + + /* parse_json() + * + * Parse a JSON object. + */ + Json parse_json(int depth) { + if (depth > max_depth) { + return fail("exceeded maximum nesting depth"); + } + + char ch = get_next_token(); + if (failed) + return Json(); + + if (ch == '-' || (ch >= '0' && ch <= '9')) { + i--; + return parse_number(); + } + + if (ch == 't') + return expect("true", true); + + if (ch == 'f') + return expect("false", false); + + if (ch == 'n') + return expect("null", Json()); + + if (ch == '"') + return parse_string(); + + if (ch == '{') { + map data; + ch = get_next_token(); + if (ch == '}') + return data; + + while (1) { + if (ch != '"') + return fail("expected '\"' in object, got " + esc(ch)); + + string key = parse_string(); + if (failed) + return Json(); + + ch = get_next_token(); + if (ch != ':') + return fail("expected ':' in object, got " + esc(ch)); + + data[std::move(key)] = parse_json(depth + 1); + if (failed) + return Json(); + + ch = get_next_token(); + if (ch == '}') + break; + if (ch != ',') + return fail("expected ',' in object, got " + esc(ch)); + + ch = get_next_token(); + } + return data; + } + + if (ch == '[') { + vector data; + ch = get_next_token(); + if (ch == ']') + return data; + + while (1) { + i--; + data.push_back(parse_json(depth + 1)); + if (failed) + return Json(); + + ch = get_next_token(); + if (ch == ']') + break; + if (ch != ',') + return fail("expected ',' in list, got " + esc(ch)); + + ch = get_next_token(); + (void)ch; + } + return data; + } + + return fail("expected value, got " + esc(ch)); + } +}; +}//namespace { + +Json Json::parse(const string &in, string &err, JsonParse strategy) { + JsonParser parser { in, 0, err, false, strategy }; + Json result = parser.parse_json(0); + + // Check for any trailing garbage + parser.consume_garbage(); + if (parser.failed) + return Json(); + if (parser.i != in.size()) + return parser.fail("unexpected trailing " + esc(in[parser.i])); + + return result; +} + +// Documented in json11.hpp +vector Json::parse_multi(const string &in, + std::string::size_type &parser_stop_pos, + string &err, + JsonParse strategy) { + JsonParser parser { in, 0, err, false, strategy }; + parser_stop_pos = 0; + vector json_vec; + while (parser.i != in.size() && !parser.failed) { + json_vec.push_back(parser.parse_json(0)); + if (parser.failed) + break; + + // Check for another object + parser.consume_garbage(); + if (parser.failed) + break; + parser_stop_pos = parser.i; + } + return json_vec; +} + +/* * * * * * * * * * * * * * * * * * * * + * Shape-checking + */ + +bool Json::has_shape(const shape & types, string & err) const { + if (!is_object()) { + err = "expected JSON object, got " + dump(); + return false; + } + + const auto& obj_items = object_items(); + for (auto & item : types) { + const auto it = obj_items.find(item.first); + if (it == obj_items.cend() || it->second.type() != item.second) { + err = "bad type for " + item.first + " in " + dump(); + return false; + } + } + + return true; +} + +} // namespace json11 diff --git a/Firmware/lib/json11/json11.hpp b/Firmware/lib/json11/json11.hpp new file mode 100644 index 0000000..0c47d05 --- /dev/null +++ b/Firmware/lib/json11/json11.hpp @@ -0,0 +1,232 @@ +/* json11 + * + * json11 is a tiny JSON library for C++11, providing JSON parsing and serialization. + * + * The core object provided by the library is json11::Json. A Json object represents any JSON + * value: null, bool, number (int or double), string (std::string), array (std::vector), or + * object (std::map). + * + * Json objects act like values: they can be assigned, copied, moved, compared for equality or + * order, etc. There are also helper methods Json::dump, to serialize a Json to a string, and + * Json::parse (static) to parse a std::string as a Json object. + * + * Internally, the various types of Json object are represented by the JsonValue class + * hierarchy. + * + * A note on numbers - JSON specifies the syntax of number formatting but not its semantics, + * so some JSON implementations distinguish between integers and floating-point numbers, while + * some don't. In json11, we choose the latter. Because some JSON implementations (namely + * Javascript itself) treat all numbers as the same type, distinguishing the two leads + * to JSON that will be *silently* changed by a round-trip through those implementations. + * Dangerous! To avoid that risk, json11 stores all numbers as double internally, but also + * provides integer helpers. + * + * Fortunately, double-precision IEEE754 ('double') can precisely store any integer in the + * range +/-2^53, which includes every 'int' on most systems. (Timestamps often use int64 + * or long long to avoid the Y2038K problem; a double storing microseconds since some epoch + * will be exact for +/- 275 years.) + */ + +/* Copyright (c) 2013 Dropbox, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#ifdef _MSC_VER + #if _MSC_VER <= 1800 // VS 2013 + #ifndef noexcept + #define noexcept throw() + #endif + + #ifndef snprintf + #define snprintf _snprintf_s + #endif + #endif +#endif + +namespace json11 { + +enum JsonParse { + STANDARD, COMMENTS +}; + +class JsonValue; + +class Json final { +public: + // Types + enum Type { + NUL, NUMBER, BOOL, STRING, ARRAY, OBJECT + }; + + // Array and object typedefs + typedef std::vector array; + typedef std::map object; + + // Constructors for the various types of JSON value. + Json() noexcept; // NUL + Json(std::nullptr_t) noexcept; // NUL + Json(double value); // NUMBER + Json(int value); // NUMBER + Json(bool value); // BOOL + Json(const std::string &value); // STRING + Json(std::string &&value); // STRING + Json(const char * value); // STRING + Json(const array &values); // ARRAY + Json(array &&values); // ARRAY + Json(const object &values); // OBJECT + Json(object &&values); // OBJECT + + // Implicit constructor: anything with a to_json() function. + template + Json(const T & t) : Json(t.to_json()) {} + + // Implicit constructor: map-like objects (std::map, std::unordered_map, etc) + template ().begin()->first)>::value + && std::is_constructible().begin()->second)>::value, + int>::type = 0> + Json(const M & m) : Json(object(m.begin(), m.end())) {} + + // Implicit constructor: vector-like objects (std::list, std::vector, std::set, etc) + template ().begin())>::value, + int>::type = 0> + Json(const V & v) : Json(array(v.begin(), v.end())) {} + + // This prevents Json(some_pointer) from accidentally producing a bool. Use + // Json(bool(some_pointer)) if that behavior is desired. + Json(void *) = delete; + + // Accessors + Type type() const; + + bool is_null() const { return type() == NUL; } + bool is_number() const { return type() == NUMBER; } + bool is_bool() const { return type() == BOOL; } + bool is_string() const { return type() == STRING; } + bool is_array() const { return type() == ARRAY; } + bool is_object() const { return type() == OBJECT; } + + // Return the enclosed value if this is a number, 0 otherwise. Note that json11 does not + // distinguish between integer and non-integer numbers - number_value() and int_value() + // can both be applied to a NUMBER-typed object. + double number_value() const; + int int_value() const; + + // Return the enclosed value if this is a boolean, false otherwise. + bool bool_value() const; + // Return the enclosed string if this is a string, "" otherwise. + const std::string &string_value() const; + // Return the enclosed std::vector if this is an array, or an empty vector otherwise. + const array &array_items() const; + // Return the enclosed std::map if this is an object, or an empty map otherwise. + const object &object_items() const; + + // Return a reference to arr[i] if this is an array, Json() otherwise. + const Json & operator[](size_t i) const; + // Return a reference to obj[key] if this is an object, Json() otherwise. + const Json & operator[](const std::string &key) const; + + // Serialize. + void dump(std::string &out) const; + std::string dump() const { + std::string out; + dump(out); + return out; + } + + // Parse. If parse fails, return Json() and assign an error message to err. + static Json parse(const std::string & in, + std::string & err, + JsonParse strategy = JsonParse::STANDARD); + static Json parse(const char * in, + std::string & err, + JsonParse strategy = JsonParse::STANDARD) { + if (in) { + return parse(std::string(in), err, strategy); + } else { + err = "null input"; + return nullptr; + } + } + // Parse multiple objects, concatenated or separated by whitespace + static std::vector parse_multi( + const std::string & in, + std::string::size_type & parser_stop_pos, + std::string & err, + JsonParse strategy = JsonParse::STANDARD); + + static inline std::vector parse_multi( + const std::string & in, + std::string & err, + JsonParse strategy = JsonParse::STANDARD) { + std::string::size_type parser_stop_pos; + return parse_multi(in, parser_stop_pos, err, strategy); + } + + bool operator== (const Json &rhs) const; + bool operator< (const Json &rhs) const; + bool operator!= (const Json &rhs) const { return !(*this == rhs); } + bool operator<= (const Json &rhs) const { return !(rhs < *this); } + bool operator> (const Json &rhs) const { return (rhs < *this); } + bool operator>= (const Json &rhs) const { return !(*this < rhs); } + + /* has_shape(types, err) + * + * Return true if this is a JSON object and, for each item in types, has a field of + * the given type. If not, return false and set err to a descriptive message. + */ + typedef std::initializer_list> shape; + bool has_shape(const shape & types, std::string & err) const; + +private: + std::shared_ptr m_ptr; +}; + +// Internal class hierarchy - JsonValue objects are not exposed to users of this API. +class JsonValue { +protected: + friend class Json; + friend class JsonInt; + friend class JsonDouble; + virtual Json::Type type() const = 0; + virtual bool equals(const JsonValue * other) const = 0; + virtual bool less(const JsonValue * other) const = 0; + virtual void dump(std::string &out) const = 0; + virtual double number_value() const; + virtual int int_value() const; + virtual bool bool_value() const; + virtual const std::string &string_value() const; + virtual const Json::array &array_items() const; + virtual const Json &operator[](size_t i) const; + virtual const Json::object &object_items() const; + virtual const Json &operator[](const std::string &key) const; + virtual ~JsonValue() {} +}; + +} // namespace json11 diff --git a/Firmware/lib/json11/json11.pc.in b/Firmware/lib/json11/json11.pc.in new file mode 100644 index 0000000..63fb241 --- /dev/null +++ b/Firmware/lib/json11/json11.pc.in @@ -0,0 +1,9 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +libdir=${prefix}/lib/@CMAKE_LIBRARY_ARCHITECTURE@ +includedir=${prefix}/include/@CMAKE_LIBRARY_ARCHITECTURE@ + +Name: @PROJECT_NAME@ +Description: json11 is a tiny JSON library for C++11, providing JSON parsing and serialization. +Version: @PROJECT_VERSION@ +Libs: -L${libdir} -ljson11 +Cflags: -I${includedir} diff --git a/Firmware/lib/json11/test.cpp b/Firmware/lib/json11/test.cpp new file mode 100644 index 0000000..3712d10 --- /dev/null +++ b/Firmware/lib/json11/test.cpp @@ -0,0 +1,286 @@ +/* + * Define JSON11_TEST_CUSTOM_CONFIG to 1 if you want to build this tester into + * your own unit-test framework rather than a stand-alone program. By setting + * The values of the variables included below, you can insert your own custom + * code into this file as it builds, in order to make it into a test case for + * your favorite framework. + */ +#if !JSON11_TEST_CUSTOM_CONFIG +#define JSON11_TEST_CPP_PREFIX_CODE +#define JSON11_TEST_CPP_SUFFIX_CODE +#define JSON11_TEST_STANDALONE_MAIN 1 +#define JSON11_TEST_CASE(name) static void name() +#define JSON11_TEST_ASSERT(b) assert(b) +#ifdef NDEBUG +#undef NDEBUG//at now assert will work even in Release build +#endif +#endif // JSON11_TEST_CUSTOM_CONFIG + +/* + * Enable or disable code which demonstrates the behavior change in Xcode 7 / Clang 3.7, + * introduced by DR1467 and described here: https://github.com/dropbox/json11/issues/86 + * Defaults to off since it doesn't appear the standards committee is likely to act + * on this, so it needs to be considered normal behavior. + */ +#ifndef JSON11_ENABLE_DR1467_CANARY +#define JSON11_ENABLE_DR1467_CANARY 0 +#endif + +/* + * Beginning of standard source file, which makes use of the customizations above. + */ +#include +#include +#include +#include +#include +#include +#include "json11.hpp" +#include +#include +#include +#include +#include + +// Insert user-defined prefix code (includes, function declarations, etc) +// to set up a custom test suite +JSON11_TEST_CPP_PREFIX_CODE + +using namespace json11; +using std::string; + +// Check that Json has the properties we want. +#define CHECK_TRAIT(x) static_assert(std::x::value, #x) +CHECK_TRAIT(is_nothrow_constructible); +CHECK_TRAIT(is_nothrow_default_constructible); +CHECK_TRAIT(is_copy_constructible); +CHECK_TRAIT(is_nothrow_move_constructible); +CHECK_TRAIT(is_copy_assignable); +CHECK_TRAIT(is_nothrow_move_assignable); +CHECK_TRAIT(is_nothrow_destructible); + +JSON11_TEST_CASE(json11_test) { + const string simple_test = + R"({"k1":"v1", "k2":42, "k3":["a",123,true,false,null]})"; + + string err; + const auto json = Json::parse(simple_test, err); + + std::cout << "k1: " << json["k1"].string_value() << "\n"; + std::cout << "k3: " << json["k3"].dump() << "\n"; + + for (auto &k : json["k3"].array_items()) { + std::cout << " - " << k.dump() << "\n"; + } + + string comment_test = R"({ + // comment /* with nested comment */ + "a": 1, + // comment + // continued + "b": "text", + /* multi + line + comment + // line-comment-inside-multiline-comment + */ + // and single-line comment + // and single-line comment /* multiline inside single line */ + "c": [1, 2, 3] + // and single-line comment at end of object + })"; + + string err_comment; + auto json_comment = Json::parse( + comment_test, err_comment, JsonParse::COMMENTS); + JSON11_TEST_ASSERT(!json_comment.is_null()); + JSON11_TEST_ASSERT(err_comment.empty()); + + comment_test = "{\"a\": 1}//trailing line comment"; + json_comment = Json::parse( + comment_test, err_comment, JsonParse::COMMENTS); + JSON11_TEST_ASSERT(!json_comment.is_null()); + JSON11_TEST_ASSERT(err_comment.empty()); + + comment_test = "{\"a\": 1}/*trailing multi-line comment*/"; + json_comment = Json::parse( + comment_test, err_comment, JsonParse::COMMENTS); + JSON11_TEST_ASSERT(!json_comment.is_null()); + JSON11_TEST_ASSERT(err_comment.empty()); + + string failing_comment_test = "{\n/* unterminated comment\n\"a\": 1,\n}"; + string err_failing_comment; + auto json_failing_comment = Json::parse( + failing_comment_test, err_failing_comment, JsonParse::COMMENTS); + JSON11_TEST_ASSERT(json_failing_comment.is_null()); + JSON11_TEST_ASSERT(!err_failing_comment.empty()); + + failing_comment_test = "{\n/* unterminated trailing comment }"; + json_failing_comment = Json::parse( + failing_comment_test, err_failing_comment, JsonParse::COMMENTS); + JSON11_TEST_ASSERT(json_failing_comment.is_null()); + JSON11_TEST_ASSERT(!err_failing_comment.empty()); + + failing_comment_test = "{\n/ / bad comment }"; + json_failing_comment = Json::parse( + failing_comment_test, err_failing_comment, JsonParse::COMMENTS); + JSON11_TEST_ASSERT(json_failing_comment.is_null()); + JSON11_TEST_ASSERT(!err_failing_comment.empty()); + + failing_comment_test = "{// bad comment }"; + json_failing_comment = Json::parse( + failing_comment_test, err_failing_comment, JsonParse::COMMENTS); + JSON11_TEST_ASSERT(json_failing_comment.is_null()); + JSON11_TEST_ASSERT(!err_failing_comment.empty()); + + failing_comment_test = "{\n\"a\": 1\n}/"; + json_failing_comment = Json::parse( + failing_comment_test, err_failing_comment, JsonParse::COMMENTS); + JSON11_TEST_ASSERT(json_failing_comment.is_null()); + JSON11_TEST_ASSERT(!err_failing_comment.empty()); + + failing_comment_test = "{/* bad\ncomment *}"; + json_failing_comment = Json::parse( + failing_comment_test, err_failing_comment, JsonParse::COMMENTS); + JSON11_TEST_ASSERT(json_failing_comment.is_null()); + JSON11_TEST_ASSERT(!err_failing_comment.empty()); + + std::list l1 { 1, 2, 3 }; + std::vector l2 { 1, 2, 3 }; + std::set l3 { 1, 2, 3 }; + JSON11_TEST_ASSERT(Json(l1) == Json(l2)); + JSON11_TEST_ASSERT(Json(l2) == Json(l3)); + + std::map m1 { { "k1", "v1" }, { "k2", "v2" } }; + std::unordered_map m2 { { "k1", "v1" }, { "k2", "v2" } }; + JSON11_TEST_ASSERT(Json(m1) == Json(m2)); + + // Json literals + const Json obj = Json::object({ + { "k1", "v1" }, + { "k2", 42.0 }, + { "k3", Json::array({ "a", 123.0, true, false, nullptr }) }, + }); + + std::cout << "obj: " << obj.dump() << "\n"; + JSON11_TEST_ASSERT(obj.dump() == "{\"k1\": \"v1\", \"k2\": 42, \"k3\": [\"a\", 123, true, false, null]}"); + + JSON11_TEST_ASSERT(Json("a").number_value() == 0); + JSON11_TEST_ASSERT(Json("a").string_value() == "a"); + JSON11_TEST_ASSERT(Json().number_value() == 0); + + JSON11_TEST_ASSERT(obj == json); + JSON11_TEST_ASSERT(Json(42) == Json(42.0)); + JSON11_TEST_ASSERT(Json(42) != Json(42.1)); + + const string unicode_escape_test = + R"([ "blah\ud83d\udca9blah\ud83dblah\udca9blah\u0000blah\u1234" ])"; + + const char utf8[] = "blah" "\xf0\x9f\x92\xa9" "blah" "\xed\xa0\xbd" "blah" + "\xed\xb2\xa9" "blah" "\0" "blah" "\xe1\x88\xb4"; + + Json uni = Json::parse(unicode_escape_test, err); + JSON11_TEST_ASSERT(uni[0].string_value().size() == (sizeof utf8) - 1); + JSON11_TEST_ASSERT(std::memcmp(uni[0].string_value().data(), utf8, sizeof utf8) == 0); + + // Demonstrates the behavior change in Xcode 7 / Clang 3.7, introduced by DR1467 + // and described here: https://llvm.org/bugs/show_bug.cgi?id=23812 + if (JSON11_ENABLE_DR1467_CANARY) { + Json nested_array = Json::array { Json::array { 1, 2, 3 } }; + JSON11_TEST_ASSERT(nested_array.is_array()); + JSON11_TEST_ASSERT(nested_array.array_items().size() == 1); + JSON11_TEST_ASSERT(nested_array.array_items()[0].is_array()); + JSON11_TEST_ASSERT(nested_array.array_items()[0].array_items().size() == 3); + } + + { + const std::string good_json = R"( {"k1" : "v1"})"; + const std::string bad_json1 = good_json + " {"; + const std::string bad_json2 = good_json + R"({"k2":"v2", "k3":[)"; + struct TestMultiParse { + std::string input; + std::string::size_type expect_parser_stop_pos; + size_t expect_not_empty_elms_count; + Json expect_parse_res; + } tests[] = { + {" {", 0, 0, {}}, + {good_json, good_json.size(), 1, Json(std::map{ { "k1", "v1" } })}, + {bad_json1, good_json.size() + 1, 1, Json(std::map{ { "k1", "v1" } })}, + {bad_json2, good_json.size(), 1, Json(std::map{ { "k1", "v1" } })}, + {"{}", 2, 1, Json::object{}}, + }; + for (const auto &tst : tests) { + std::string::size_type parser_stop_pos; + std::string err; + auto res = Json::parse_multi(tst.input, parser_stop_pos, err); + JSON11_TEST_ASSERT(parser_stop_pos == tst.expect_parser_stop_pos); + JSON11_TEST_ASSERT( + (size_t)std::count_if(res.begin(), res.end(), + [](const Json& j) { return !j.is_null(); }) + == tst.expect_not_empty_elms_count); + if (!res.empty()) { + JSON11_TEST_ASSERT(tst.expect_parse_res == res[0]); + } + } + } + + Json my_json = Json::object { + { "key1", "value1" }, + { "key2", false }, + { "key3", Json::array { 1, 2, 3 } }, + }; + std::string json_obj_str = my_json.dump(); + std::cout << "json_obj_str: " << json_obj_str << "\n"; + JSON11_TEST_ASSERT(json_obj_str == "{\"key1\": \"value1\", \"key2\": false, \"key3\": [1, 2, 3]}"); + + class Point { + public: + int x; + int y; + Point (int x, int y) : x(x), y(y) {} + Json to_json() const { return Json::array { x, y }; } + }; + + std::vector points = { { 1, 2 }, { 10, 20 }, { 100, 200 } }; + std::string points_json = Json(points).dump(); + std::cout << "points_json: " << points_json << "\n"; + JSON11_TEST_ASSERT(points_json == "[[1, 2], [10, 20], [100, 200]]"); + + JSON11_TEST_ASSERT(((Json)(Json::object { { "foo", nullptr } })).has_shape({ { "foo", Json::NUL } }, err) == true); + JSON11_TEST_ASSERT(((Json)(Json::object { { "foo", 1234567 } })).has_shape({ { "foo", Json::NUL } }, err) == false); + JSON11_TEST_ASSERT(((Json)(Json::object { { "bar", 1234567 } })).has_shape({ { "foo", Json::NUL } }, err) == false); + +} + +#if JSON11_TEST_STANDALONE_MAIN + +static void parse_from_stdin() { + string buf; + string line; + while (std::getline(std::cin, line)) { + buf += line + "\n"; + } + + string err; + auto json = Json::parse(buf, err); + if (!err.empty()) { + printf("Failed: %s\n", err.c_str()); + } else { + printf("Result: %s\n", json.dump().c_str()); + } +} + +int main(int argc, char **argv) { + if (argc == 2 && argv[1] == string("--stdin")) { + parse_from_stdin(); + return 0; + } + + json11_test(); +} + +#endif // JSON11_TEST_STANDALONE_MAIN + +// Insert user-defined suffix code (function definitions, etc) +// to set up a custom test suite +JSON11_TEST_CPP_SUFFIX_CODE diff --git a/Firmware/platformio.ini b/Firmware/platformio.ini new file mode 100644 index 0000000..d0cff6a --- /dev/null +++ b/Firmware/platformio.ini @@ -0,0 +1,56 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:main] +platform = espressif32 +board = esp32doit-devkit-v1 +framework = arduino +monitor_speed = 115200 +upload_protocol = esptool +#upload_flags = -b 115200 + +monitor_flags = + --eol=CRLF + --echo + --filter=esp32_exception_decoder +lib_deps = + TFT_eSPI@2.5.43 + bitbank2/AnimatedGIF @ ^1.4.4 + bxparks/AceButton @ ^1.9.1 + +build_type = release +; board_build.partitions = default_8MB.csv + +build_flags = + -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG + + ; TFT_eSPI setup: + -DUSER_SETUP_LOADED=1 + -DST7789_DRIVER=1 + -DCGRAM_OFFSET=1 + -DTFT_WIDTH=135 + -DTFT_HEIGHT=240 + -DTFT_MISO=-1 + -DTFT_MOSI=22 + -DTFT_SCLK=21 + -DTFT_CS=25 + -DTFT_DC=19 + -DTFT_RST=5 + -DLOAD_GLCD=1 + -DLOAD_GFXFF=1 + -DSPI_FREQUENCY=40000000 + +[env:mainOTA] +extends = env:main + +upload_protocol = espota +upload_port = switchornament.local +upload_flags = + --auth="hunter2" diff --git a/Firmware/src/display_task.cpp b/Firmware/src/display_task.cpp new file mode 100644 index 0000000..17bc965 --- /dev/null +++ b/Firmware/src/display_task.cpp @@ -0,0 +1,419 @@ +#include "display_task.h" + +#include +#include +#include +#include +#include + +#include + +#include "gif_player.h" + +using namespace json11; + +#define PIN_LCD_BACKLIGHT 27 + +#define PIN_SD_DAT1 4 +#define PIN_SD_DAT2 12 + +DisplayTask::DisplayTask(MainTask& main_task, const uint8_t task_core) : Task{"Display", 8192, 1, task_core}, Logger(), main_task_(main_task) { + log_queue_ = xQueueCreate(10, sizeof(std::string *)); + assert(log_queue_ != NULL); + + event_queue_ = xQueueCreate(10, sizeof(Event)); + assert(event_queue_ != NULL); +} + +int DisplayTask::enumerateGifs(const char* basePath, std::vector& out_files) { + int amount = 0; + File GifRootFolder = SD_MMC.open(basePath); + if(!GifRootFolder){ + log_n("Failed to open directory"); + return 0; + } + + if(!GifRootFolder.isDirectory()){ + log_n("Not a directory"); + return 0; + } + + File file = GifRootFolder.openNextFile(); + + while( file ) { + if(!file.isDirectory()) { + if(file.name()[0] != '.') + { + String FullPath; + FullPath = String(basePath) + "/" + String(file.name()); + out_files.push_back( FullPath.c_str() ); + log_d("got file: %s",file.name()); + amount++; + } + file.close(); + } + file = GifRootFolder.openNextFile(); + } + GifRootFolder.close(); + log_n("Found %d GIF files", amount); + return amount; +} + + +// perform the actual update from a given stream +bool DisplayTask::performUpdate(Stream &updateSource, size_t updateSize) { + if (Update.begin(updateSize)) { + size_t written = Update.writeStream(updateSource); + if (written == updateSize) { + Serial.println("Written : " + String(written) + " successfully"); + } + else { + Serial.println("Written only : " + String(written) + "/" + String(updateSize) + ". Retry?"); + } + if (Update.end()) { + Serial.println("OTA done!"); + if (Update.isFinished()) { + Serial.println("Update successfully completed. Rebooting."); + tft_.fillScreen(TFT_BLACK); + tft_.drawString("Update successful!", 0, 0); + return true; + } + else { + Serial.println("Update not finished? Something went wrong!"); + tft_.fillScreen(TFT_BLACK); + tft_.drawString("Update error: unknown", 0, 0); + } + } + else { + uint8_t error = Update.getError(); + Serial.println("Error Occurred. Error #: " + String(error)); + tft_.fillScreen(TFT_BLACK); + tft_.drawString("Update error: " + String(error), 0, 0); + } + + } + else + { + Serial.println("Not enough space to begin OTA"); + tft_.fillScreen(TFT_BLACK); + tft_.drawString("Not enough space", 0, 0); + } + return false; +} + +// check given FS for valid firmware.bin and perform update if available +bool DisplayTask::updateFromFS(fs::FS &fs) { + tft_.fillScreen(TFT_BLACK); + tft_.setTextDatum(TL_DATUM); + + File updateBin = fs.open("/firmware.bin"); + if (updateBin) { + if(updateBin.isDirectory()){ + Serial.println("Error, firmware.bin is not a file"); + updateBin.close(); + return false; + } + + size_t updateSize = updateBin.size(); + + bool update_successful = false; + if (updateSize > 0) { + Serial.println("Try to start update"); + digitalWrite(PIN_LCD_BACKLIGHT, HIGH); + tft_.fillScreen(TFT_BLACK); + tft_.drawString("Starting update...", 0, 0); + delay(1000); + update_successful = performUpdate(updateBin, updateSize); + } + else { + Serial.println("Error, file is empty"); + } + + updateBin.close(); + fs.remove("/firmware.bin"); + + // Leave some time to read the update result message + delay(5000); + return update_successful; + } + else { + Serial.println("No firmware.bin at sd root"); + return false; + } +} + +void DisplayTask::run() { + pinMode(PIN_LCD_BACKLIGHT, OUTPUT); + pinMode(PIN_SD_DAT1, INPUT_PULLUP); + pinMode(PIN_SD_DAT2, INPUT_PULLUP); + + tft_.begin(); +#ifdef USE_DMA + tft_.initDMA(); +#endif + tft_.setRotation(1); + tft_.fillScreen(TFT_BLACK); + + bool isblinked = false; + while(! SD_MMC.begin("/sdcard", false) ) { + digitalWrite(PIN_LCD_BACKLIGHT, HIGH); + log_n("SD Card mount failed!"); + isblinked = !isblinked; + if( isblinked ) { + tft_.setTextColor( TFT_WHITE, TFT_BLACK ); + } else { + tft_.setTextColor( TFT_BLACK, TFT_WHITE ); + } + tft_.setTextDatum(TC_DATUM); + tft_.drawString( "INSERT SD", tft_.width()/2, tft_.height()/2 ); + + delay( 300 ); + } + + log_n("SD Card mounted!"); + + if (updateFromFS(SD_MMC)) { + ESP.restart(); + } + + // ##################################################### + // CHANGES ABOVE THIS LINE MAY BREAK FIRMWARE UPDATES!!! + // ##################################################### + + main_task_.setLogger(this); + + // Load config from SD card + File configFile = SD_MMC.open("/config.json"); + if (configFile) { + if(configFile.isDirectory()){ + log("Error, config.json is not a file"); + } else { + char data[512]; + size_t data_len = configFile.readBytes(data, sizeof(data) - 1); + data[data_len] = 0; + + std::string err; + Json json = Json::parse(data, err); + if (err.empty()) { + show_log_ = json["show_log"].bool_value(); + const char* ssid = json["ssid"].string_value().c_str(); + const char* password = json["password"].string_value().c_str(); + Serial.printf("Wifi info: %s %s\n", ssid, password); + + const char* tz = json["timezone"].string_value().c_str(); + Serial.printf("Timezone: %s\n", tz); + + main_task_.setConfig(ssid, password, tz); + } else { + log("Error parsing wifi credentials! " + String(err.c_str())); + } + } + configFile.close(); + } else { + log("Missing config file!"); + } + + // Delay to avoid brownout while wifi is starting + delay(500); + + GifPlayer::begin(&tft_); + + if (GifPlayer::start("/gifs/boot.gif")) { + GifPlayer::play_frame(nullptr); + delay(50); + digitalWrite(PIN_LCD_BACKLIGHT, HIGH); + delay(200); + while (GifPlayer::play_frame(nullptr)) { + yield(); + } + digitalWrite(PIN_LCD_BACKLIGHT, LOW); + delay(500); + GifPlayer::stop(); + } + + std::vector main_gifs; + std::vector christmas_gifs; + + int num_main_gifs = enumerateGifs( "/gifs/main", main_gifs); + int num_christmas_gifs = enumerateGifs( "/gifs/christmas", christmas_gifs); + int current_file = -1; + const char* current_file_name = ""; + uint32_t minimum_loop_duration = 0; + uint32_t start_millis = UINT32_MAX; + + bool last_christmas; // I gave you my heart... + + main_task_.registerEventQueue(event_queue_); + + State state = State::CHOOSE_GIF; + int frame_delay = 0; + uint32_t last_frame = 0; + while (1) { + bool left_button = false; + bool right_button = false; + Event event; + if (xQueueReceive(event_queue_, &event, 0)) { + switch (event.type) { + case EventType::BUTTON: + if (event.button.event == ace_button::AceButton::kEventPressed) { + if (event.button.button_id == BUTTON_ID_LEFT) { + left_button = true; + } else if (event.button.button_id == BUTTON_ID_RIGHT) { + right_button = true; + } + } + break; + } + } + handleLogRendering(); + switch (state) { + case State::CHOOSE_GIF: + Serial.println("Choose gif"); + if (millis() - start_millis > minimum_loop_duration) { + // Only change the file if we've exceeded the minimum loop duration + if (isChristmas()) { + if (num_christmas_gifs > 0) { + current_file_name = christmas_gifs[current_file++ % num_christmas_gifs].c_str(); + minimum_loop_duration = 30000; + Serial.printf("Chose christmas gif: %s\n", current_file_name); + } else { + continue; + } + } else { + if (num_main_gifs > 0) { + int next_file = current_file; + while (num_main_gifs > 1 && next_file == current_file) { + next_file = random(num_main_gifs); + } + current_file = next_file; + current_file_name = main_gifs[current_file].c_str(); + minimum_loop_duration = 0; + Serial.printf("Chose gif: %s\n", current_file_name); + } else { + continue; + } + } + start_millis = millis(); + } + if (!GifPlayer::start(current_file_name)) { + continue; + } + last_frame = millis(); + GifPlayer::play_frame(&frame_delay); + delay(50); + digitalWrite(PIN_LCD_BACKLIGHT, HIGH); + state = State::PLAY_GIF; + break; + case State::PLAY_GIF: { + if (right_button) { + GifPlayer::stop(); + int center = tft_.width()/2; + tft_.fillScreen(TFT_BLACK); + tft_.setTextSize(2); + tft_.setTextDatum(TC_DATUM); + tft_.drawString("Merry Christmas!", center, 10); + tft_.setTextSize(1); + tft_.drawString("Designed and handmade", center, 50); + tft_.drawString("by Scott Bezek", center, 60); + tft_.drawString("Oakland, 2021", center, 80); + + if (WiFi.status() == WL_CONNECTED) { + tft_.setTextDatum(BL_DATUM); + tft_.drawString(String("IP: ") + WiFi.localIP().toString(), 5, tft_.height()); + } + main_task_.setOtaEnabled(true); + delay(200); + state = State::SHOW_CREDITS; + break; + } + bool is_christmas = isChristmas(); + bool christmas_changed = false; + if (is_christmas != last_christmas) { + last_christmas = is_christmas; + christmas_changed = true; + } + + if (left_button || christmas_changed) { + // Force select new gif, even if we hadn't met the minimum loop duration yet + minimum_loop_duration = 0; + GifPlayer::stop(); + state = State::CHOOSE_GIF; + break; + } + uint32_t time_since_last_frame = millis() - last_frame; + if (time_since_last_frame > frame_delay) { + // Time for the next frame; play it + last_frame = millis(); + if (!GifPlayer::play_frame(&frame_delay)) { + GifPlayer::stop(); + state = State::CHOOSE_GIF; + break; + } + } else { + // Wait until it's time for the next frame, but up to 50ms max at a time to avoid stalling UI thread + delay(min((uint32_t)50, frame_delay - time_since_last_frame)); + } + + break; + } + case State::SHOW_CREDITS: + if (right_button) { + // Exit credits + main_task_.setOtaEnabled(false); + state = State::CHOOSE_GIF; + tft_.fillScreen(TFT_BLACK); + delay(200); + } + break; + } + } +} + +bool DisplayTask::isChristmas() { + tm local; + return main_task_.getLocalTime(&local) && local.tm_mon == 11 && local.tm_mday == 25; +} + +void DisplayTask::handleLogRendering() { + uint32_t now = millis(); + // Check for new message + bool force_redraw = false; + if (now - last_message_millis_ > 100) { + std::string* log_string; + if (xQueueReceive(log_queue_, &log_string, 0) == pdTRUE) { + last_message_millis_ = now; + force_redraw = true; + strncpy(current_message_, log_string->c_str(), sizeof(current_message_)); + delete log_string; + } + } + + bool show = show_log_ && (now - last_message_millis_ < 3000); + + if (show && (!message_visible_ || force_redraw)) { + GifPlayer::set_max_line(124); + tft_.fillRect(0, 124, DISPLAY_WIDTH, 11, TFT_BLACK); + tft_.setTextSize(1); + tft_.setTextDatum(TL_DATUM); + tft_.drawString(current_message_, 3, 126); + } else if (!show && message_visible_) { + tft_.fillRect(0, 124, DISPLAY_WIDTH, 11, TFT_BLACK); + GifPlayer::set_max_line(-1); + } + message_visible_ = show; +} + +void DisplayTask::log(const char* msg) { + Serial.println(msg); + // Allocate a string for the duration it's in the queue; it is free'd by the queue consumer + std::string* msg_str = new std::string(msg); + + // Put string in queue (or drop if full to avoid blocking) + if (xQueueSendToBack(log_queue_, &msg_str, 0) != pdTRUE) { + delete msg_str; + } +} + +void DisplayTask::log(String msg) { + log(msg.c_str()); +} diff --git a/Firmware/src/display_task.h b/Firmware/src/display_task.h new file mode 100644 index 0000000..4248322 --- /dev/null +++ b/Firmware/src/display_task.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +#include "logger.h" +#include "main_task.h" +#include "task.h" + +enum class State { + CHOOSE_GIF, + PLAY_GIF, + SHOW_CREDITS, +}; + +class DisplayTask : public Task, public Logger { + friend class Task; // Allow base Task to invoke protected run() + + public: + DisplayTask(MainTask& main_task, const uint8_t task_core); + virtual ~DisplayTask() {}; + + void log(const char* msg) override; + + protected: + void run(); + + private: + bool performUpdate(Stream &updateSource, size_t updateSize); + bool updateFromFS(fs::FS &fs); + int enumerateGifs( const char* basePath, std::vector& out_files); + bool isChristmas(); + void handleLogRendering(); + + void log(String msg); + + TFT_eSPI tft_ = TFT_eSPI(); + MainTask& main_task_; + QueueHandle_t log_queue_; + QueueHandle_t event_queue_; + + bool show_log_ = false; + bool message_visible_ = false; + char current_message_[200]; + uint32_t last_message_millis_ = UINT32_MAX; + +}; diff --git a/Firmware/src/event.h b/Firmware/src/event.h new file mode 100644 index 0000000..9b8636f --- /dev/null +++ b/Firmware/src/event.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#define BUTTON_ID_LEFT 0 +#define BUTTON_ID_RIGHT 1 + +enum class EventType { + BUTTON, +}; + +struct EventButton { + uint8_t button_id; + uint8_t event; +}; + +struct Event { + EventType type; + union { + EventButton button; + }; +}; diff --git a/Firmware/src/gif_player.cpp b/Firmware/src/gif_player.cpp new file mode 100644 index 0000000..4ac0201 --- /dev/null +++ b/Firmware/src/gif_player.cpp @@ -0,0 +1,225 @@ +#include "gif_player.h" + +#include +#include +#include + +AnimatedGIF GifPlayer::gif; +TFT_eSPI* GifPlayer::tft; + +File GifPlayer::FSGifFile; // temp gif file holder + +#ifdef USE_DMA +uint16_t GifPlayer::usTemp[2][BUFFER_SIZE]; // Global to support DMA use +#else +uint16_t GifPlayer::usTemp[1][BUFFER_SIZE]; // Global to support DMA use +#endif +bool GifPlayer::dmaBuf; + +int GifPlayer::frame_delay; +int GifPlayer::max_line = -1; + + +void * GifPlayer::GIFOpenFile(const char *fname, int32_t *pSize) +{ + //log_d("GIFOpenFile( %s )\n", fname ); + FSGifFile = SD_MMC.open(fname); + if (FSGifFile) { + *pSize = FSGifFile.size(); + return (void *)&FSGifFile; + } + return NULL; +} + + +void GifPlayer::GIFCloseFile(void *pHandle) +{ + File *f = static_cast(pHandle); + if (f != NULL) + f->close(); +} + + +int32_t GifPlayer::GIFReadFile(GIFFILE *pFile, uint8_t *pBuf, int32_t iLen) +{ + int32_t iBytesRead; + iBytesRead = iLen; + File *f = static_cast(pFile->fHandle); + // Note: If you read a file all the way to the last byte, seek() stops working + if ((pFile->iSize - pFile->iPos) < iLen) + iBytesRead = pFile->iSize - pFile->iPos - 1; // <-- ugly work-around + if (iBytesRead <= 0) + return 0; + iBytesRead = (int32_t)f->read(pBuf, iBytesRead); + pFile->iPos = f->position(); + return iBytesRead; +} + + +int32_t GifPlayer::GIFSeekFile(GIFFILE *pFile, int32_t iPosition) +{ + int i = micros(); + File *f = static_cast(pFile->fHandle); + f->seek(iPosition); + pFile->iPos = (int32_t)f->position(); + i = micros() - i; + //log_d("Seek time = %d us\n", i); + return pFile->iPos; +} + + + + +// From AnimatedGIF TFT_eSPI_memory example + +// Draw a line of image directly on the LCD +void GifPlayer::GIFDraw(GIFDRAW *pDraw) +{ + uint8_t *s; + uint16_t *d, *usPalette; + int x, y, iWidth, iCount; + + // Displ;ay bounds chech and cropping + iWidth = pDraw->iWidth; + if (iWidth + pDraw->iX > DISPLAY_WIDTH) + iWidth = DISPLAY_WIDTH - pDraw->iX; + usPalette = pDraw->pPalette; + y = pDraw->iY + pDraw->y; // current line + if (y >= DISPLAY_HEIGHT || pDraw->iX >= DISPLAY_WIDTH || iWidth < 1 || (max_line > -1 && y > max_line)) + return; + + // Old image disposal + s = pDraw->pPixels; + if (pDraw->ucDisposalMethod == 2) // restore to background color + { + for (x = 0; x < iWidth; x++) + { + if (s[x] == pDraw->ucTransparent) + s[x] = pDraw->ucBackground; + } + pDraw->ucHasTransparency = 0; + } + + // Apply the new pixels to the main image + if (pDraw->ucHasTransparency) // if transparency used + { + uint8_t *pEnd, c, ucTransparent = pDraw->ucTransparent; + pEnd = s + iWidth; + x = 0; + iCount = 0; // count non-transparent pixels + while (x < iWidth) + { + c = ucTransparent - 1; + d = &usTemp[0][0]; + while (c != ucTransparent && s < pEnd && iCount < BUFFER_SIZE ) + { + c = *s++; + if (c == ucTransparent) // done, stop + { + s--; // back up to treat it like transparent + } + else // opaque + { + *d++ = usPalette[c]; + iCount++; + } + } // while looking for opaque pixels + if (iCount) // any opaque pixels? + { + // DMA would degrtade performance here due to short line segments + tft->setAddrWindow(pDraw->iX + x, y, iCount, 1); + tft->pushPixels(usTemp, iCount); + x += iCount; + iCount = 0; + } + // no, look for a run of transparent pixels + c = ucTransparent; + while (c == ucTransparent && s < pEnd) + { + c = *s++; + if (c == ucTransparent) + x++; + else + s--; + } + } + } + else + { + s = pDraw->pPixels; + + // Unroll the first pass to boost DMA performance + // Translate the 8-bit pixels through the RGB565 palette (already byte reversed) + if (iWidth <= BUFFER_SIZE) + for (iCount = 0; iCount < iWidth; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++]; + else + for (iCount = 0; iCount < BUFFER_SIZE; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++]; + +#ifdef USE_DMA // 71.6 fps (ST7796 84.5 fps) + tft->dmaWait(); + tft->setAddrWindow(pDraw->iX, y, iWidth, 1); + tft->pushPixelsDMA(&usTemp[dmaBuf][0], iCount); + dmaBuf = !dmaBuf; +#else // 57.0 fps + tft->setAddrWindow(pDraw->iX, y, iWidth, 1); + tft->pushPixels(&usTemp[0][0], iCount); +#endif + + iWidth -= iCount; + // Loop if pixel buffer smaller than width + while (iWidth > 0) + { + // Translate the 8-bit pixels through the RGB565 palette (already byte reversed) + if (iWidth <= BUFFER_SIZE) + for (iCount = 0; iCount < iWidth; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++]; + else + for (iCount = 0; iCount < BUFFER_SIZE; iCount++) usTemp[dmaBuf][iCount] = usPalette[*s++]; + +#ifdef USE_DMA + tft->dmaWait(); + tft->pushPixelsDMA(&usTemp[dmaBuf][0], iCount); + dmaBuf = !dmaBuf; +#else + tft->pushPixels(&usTemp[0][0], iCount); +#endif + iWidth -= iCount; + } + } +} /* GIFDraw() */ + + + + +bool GifPlayer::start(const char* path) { + gif.begin(BIG_ENDIAN_PIXELS); + + if( ! gif.open( path, GIFOpenFile, GIFCloseFile, GIFReadFile, GIFSeekFile, GIFDraw ) ) { + log_n("Could not open gif %s", path ); + return false; + } + + tft->startWrite(); + return true; +} + +bool GifPlayer::play_frame(int* frame_delay) { + bool sync = frame_delay == nullptr; + return gif.playFrame(sync, frame_delay) == 1; + +} + +void GifPlayer::stop() { + gif.close(); + tft->endWrite(); + gif.reset(); +} + +void GifPlayer::begin(TFT_eSPI* tft) { + GifPlayer::tft = tft; + + dmaBuf = 0; +} + +void GifPlayer::set_max_line(int l) { + max_line = l; +} diff --git a/Firmware/src/gif_player.h b/Firmware/src/gif_player.h new file mode 100644 index 0000000..71f0c53 --- /dev/null +++ b/Firmware/src/gif_player.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include + +#define DISPLAY_WIDTH 240 +#define DISPLAY_HEIGHT 135 +// #define USE_DMA 1 +#define BUFFER_SIZE 256 // Optimum is >= GIF width or integral division of width + +class GifPlayer { + private: + static AnimatedGIF gif; + static TFT_eSPI* tft; + + static File FSGifFile; // temp gif file holder + +#ifdef USE_DMA + static uint16_t usTemp[2][BUFFER_SIZE]; // Global to support DMA use +#else + static uint16_t usTemp[1][BUFFER_SIZE]; // Global to support DMA use +#endif + static bool dmaBuf; + + static int frame_delay; + static int max_line; + + static void * GIFOpenFile(const char *fname, int32_t *pSize); + static void GIFCloseFile(void *pHandle); + static int32_t GIFReadFile(GIFFILE *pFile, uint8_t *pBuf, int32_t iLen); + static int32_t GIFSeekFile(GIFFILE *pFile, int32_t iPosition); + static void GIFDraw(GIFDRAW *pDraw); + + public: + static void begin(TFT_eSPI* tft); + + static bool start(const char* path); + static bool play_frame(int* frame_delay); + static void stop(); + + static void set_max_line(int l); + +}; diff --git a/Firmware/src/logger.h b/Firmware/src/logger.h new file mode 100644 index 0000000..914caf5 --- /dev/null +++ b/Firmware/src/logger.h @@ -0,0 +1,9 @@ +#pragma once + +class Logger { + public: + Logger() {}; + virtual ~Logger() {}; + virtual void log(const char* msg) = 0; + +}; diff --git a/Firmware/src/main.cpp b/Firmware/src/main.cpp new file mode 100644 index 0000000..4189e2f --- /dev/null +++ b/Firmware/src/main.cpp @@ -0,0 +1,21 @@ +#include + +#include "display_task.h" +#include "main_task.h" + +MainTask main_task = MainTask(0); +DisplayTask display_task = DisplayTask(main_task, 1); + +void setup() { + Serial.begin(115200); + + main_task.begin(); + display_task.begin(); + + vTaskDelete(NULL); +} + + +void loop() { + assert(false); +} \ No newline at end of file diff --git a/Firmware/src/main_task.cpp b/Firmware/src/main_task.cpp new file mode 100644 index 0000000..46c9591 --- /dev/null +++ b/Firmware/src/main_task.cpp @@ -0,0 +1,219 @@ +#include "main_task.h" + +#include +#include +#include +#include +#include + +#include "semaphore_guard.h" + +#define TASK_NOTIFY_SET_CONFIG (1 << 0) + +#define MDNS_NAME "switchOrnament" +#define OTA_PASSWORD "hunter2" + +#define PIN_LEFT_BUTTON 32 +#define PIN_RIGHT_BUTTON 26 + +using namespace ace_button; + +MainTask::MainTask(const uint8_t task_core) : Task{"Main", 8192, 1, task_core}, semaphore_(xSemaphoreCreateMutex()) { + assert(semaphore_ != NULL); + xSemaphoreGive(semaphore_); +} + +MainTask::~MainTask() { + if (semaphore_ != NULL) { + vSemaphoreDelete(semaphore_); + } +} + +void MainTask::run() { + WiFi.mode(WIFI_STA); + + AceButton left_button(PIN_LEFT_BUTTON, 1, BUTTON_ID_LEFT); + AceButton right_button(PIN_RIGHT_BUTTON, 1, BUTTON_ID_RIGHT); + + pinMode(PIN_LEFT_BUTTON, INPUT_PULLUP); + pinMode(PIN_RIGHT_BUTTON, INPUT_PULLUP); + + ButtonConfig* config = ButtonConfig::getSystemButtonConfig(); + config->setIEventHandler(this); + + ArduinoOTA + .onStart([this]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + type = "(flash)"; + else // U_SPIFFS + type = "(filesystem)"; + + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + log("Start OTA " + type); + }) + .onEnd([this]() { + log("OTA End"); + }) + .onProgress([this](unsigned int progress, unsigned int total) { + static uint32_t last_progress; + if (millis() - last_progress > 1000) { + log("OTA Progress: " + String((int)(progress * 100 / total)) + "%"); + last_progress = millis(); + } + }) + .onError([this](ota_error_t error) { + log("Error[%u]: " + String(error)); + if (error == OTA_AUTH_ERROR) log("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) log("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) log("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) log("Receive Failed"); + else if (error == OTA_END_ERROR) log("End Failed"); + }); + ArduinoOTA.setHostname(MDNS_NAME); + ArduinoOTA.setPassword(OTA_PASSWORD); + + wl_status_t wifi_status = WL_DISCONNECTED; + while (1) { + uint32_t notify_value = 0; + if (xTaskNotifyWait(0, ULONG_MAX, ¬ify_value, 0) == pdTRUE) { + if (notify_value && TASK_NOTIFY_SET_CONFIG) { + String wifi_ssid, wifi_password, timezone; + { + SemaphoreGuard lock(semaphore_); + wifi_ssid = wifi_ssid_; + wifi_password = wifi_password_; + timezone = timezone_; + } + setenv("TZ", timezone.c_str(), 1); + tzset(); + + char buf[200]; + snprintf(buf, sizeof(buf), "Connecting to %s...", wifi_ssid.c_str()); + log(buf); + WiFi.begin(wifi_ssid.c_str(), wifi_password.c_str()); + } + } + + wl_status_t new_status = WiFi.status(); + if (new_status != wifi_status) { + char buf[200]; + snprintf(buf, sizeof(buf), "Wifi status changed to %d\n", new_status); + log(buf); + if (new_status == WL_CONNECTED) { + snprintf(buf, sizeof(buf), "IP: %s", WiFi.localIP().toString().c_str()); + log(buf); + + delay(100); + // Sync SNTP + sntp_setoperatingmode(SNTP_OPMODE_POLL); + + char server[] = "time.nist.gov"; // sntp_setservername takes a non-const char*, so use a non-const variable to avoid warning + sntp_setservername(0, server); + sntp_init(); + } + + wifi_status = new_status; + } + + + time_t now = 0; + bool ntp_just_synced = false; + { + SemaphoreGuard lock(semaphore_); + if (!ntp_synced_) { + // Check if NTP has synced yet + time(&now); + if (now > 1625099485) { + ntp_just_synced = true; + ntp_synced_ = true; + } + } + } + + if (ntp_just_synced) { + // We do this separately from above to avoid deadlock: log() requires semaphore_ and we're non-reentrant-locking + char buf[200]; + strftime(buf, sizeof(buf), "Got time: %Y-%m-%d %H:%M:%S", localtime(&now)); + Serial.printf("%s\n", buf); + log(buf); + } + + ArduinoOTA.handle(); + left_button.check(); + right_button.check(); + delay(1); + } +} + +void MainTask::setConfig(const char* wifi_ssid, const char* wifi_password, const char* timezone) { + { + SemaphoreGuard lock(semaphore_); + wifi_ssid_ = String(wifi_ssid); + wifi_password_ = String(wifi_password); + timezone_ = String(timezone); + } + xTaskNotify(getHandle(), TASK_NOTIFY_SET_CONFIG, eSetBits); +} + +bool MainTask::getLocalTime(tm* t) { + SemaphoreGuard lock(semaphore_); + if (!ntp_synced_) { + return false; + } + time_t now = 0; + time(&now); + localtime_r(&now, t); + return true; +} + +void MainTask::setLogger(Logger* logger) { + SemaphoreGuard lock(semaphore_); + logger_ = logger; +} + +void MainTask::setOtaEnabled(bool enabled) { + if (enabled) { + ArduinoOTA.begin(); + } else { + ArduinoOTA.end(); + } +} + +void MainTask::log(const char* message) { + SemaphoreGuard lock(semaphore_); + if (logger_ != nullptr) { + logger_->log(message); + } else { + Serial.println(message); + } +} + +void MainTask::log(String message) { + log(message.c_str()); +} + +void MainTask::registerEventQueue(QueueHandle_t queue) { + SemaphoreGuard lock(semaphore_); + event_queues_.push_back(queue); +} + +void MainTask::publishEvent(Event event) { + SemaphoreGuard lock(semaphore_); + for (QueueHandle_t queue : event_queues_) { + xQueueSend(queue, &event, 0); + } +} + +void MainTask::handleEvent(AceButton* button, uint8_t event_type, uint8_t button_state) { + Event event = { + .type = EventType::BUTTON, + { + .button = { + .button_id = button->getId(), + .event = event_type, + }, + } + }; + publishEvent(event); +} diff --git a/Firmware/src/main_task.h b/Firmware/src/main_task.h new file mode 100644 index 0000000..5ceccfa --- /dev/null +++ b/Firmware/src/main_task.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#include + +#include "event.h" +#include "logger.h" +#include "task.h" + +class MainTask : public Task, public ace_button::IEventHandler { + friend class Task; // Allow base Task to invoke protected run() + + public: + MainTask(const uint8_t task_core); + virtual ~MainTask(); + + void setConfig(const char* wifi_ssid, const char* wifi_password, const char* timezone); + bool getLocalTime(tm* t); + void setLogger(Logger* logger); + void setOtaEnabled(bool enabled); + void registerEventQueue(QueueHandle_t queue); + + void handleEvent(ace_button::AceButton* button, uint8_t event_type, uint8_t button_state) override; + + protected: + void run(); + + private: + + void log(const char* message); + void log(String message); + + void publishEvent(Event event); + + SemaphoreHandle_t semaphore_; + + String wifi_ssid_; + String wifi_password_; + String timezone_; + + bool ntp_synced_ = false; + + Logger* logger_ = nullptr; + + std::vector event_queues_; +}; diff --git a/Firmware/src/semaphore_guard.h b/Firmware/src/semaphore_guard.h new file mode 100644 index 0000000..0f9a2a1 --- /dev/null +++ b/Firmware/src/semaphore_guard.h @@ -0,0 +1,33 @@ +/* + Copyright 2020 Scott Bezek and the splitflap contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#pragma once + +#include + +class SemaphoreGuard { + public: + SemaphoreGuard(SemaphoreHandle_t handle) : handle_{handle} { + xSemaphoreTake(handle_, portMAX_DELAY); + } + ~SemaphoreGuard() { + xSemaphoreGive(handle_); + } + SemaphoreGuard(SemaphoreGuard const&)=delete; + SemaphoreGuard& operator=(SemaphoreGuard const&)=delete; + + private: + SemaphoreHandle_t handle_; +}; diff --git a/Firmware/src/task.h b/Firmware/src/task.h new file mode 100644 index 0000000..b59c0d2 --- /dev/null +++ b/Firmware/src/task.h @@ -0,0 +1,54 @@ +/* + Copyright 2020 Scott Bezek and the splitflap contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#pragma once + +#include + +// Static polymorphic abstract base class for a FreeRTOS task using CRTP pattern. Concrete implementations +// should implement a run() method. +// Inspired by https://fjrg76.wordpress.com/2018/05/23/objectifying-task-creation-in-freertos-ii/ +template +class Task { + public: + Task(const char* name, uint32_t stackDepth, UBaseType_t priority, const BaseType_t coreId = tskNO_AFFINITY) : + name { name }, + stackDepth {stackDepth}, + priority { priority }, + coreId { coreId } + {} + virtual ~Task() {}; + + TaskHandle_t getHandle() { + return taskHandle; + } + + void begin() { + BaseType_t result = xTaskCreatePinnedToCore(taskFunction, name, stackDepth, this, priority, &taskHandle, coreId); + assert("Failed to create task" && result == pdPASS); + } + + private: + static void taskFunction(void* params) { + T* t = static_cast(params); + t->run(); + } + + const char* name; + uint32_t stackDepth; + UBaseType_t priority; + TaskHandle_t taskHandle; + const BaseType_t coreId; +}; diff --git a/Firmware/test/README b/Firmware/test/README new file mode 100644 index 0000000..df5066e --- /dev/null +++ b/Firmware/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html