added code from gh
This commit is contained in:
20
Firmware/.gitignore
vendored
Normal file
20
Firmware/.gitignore
vendored
Normal file
@@ -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
|
||||
201
Firmware/LICENSE
Normal file
201
Firmware/LICENSE
Normal file
@@ -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.
|
||||
7
Firmware/README.md
Normal file
7
Firmware/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
Reference code for my homemade Nintendo Switch ornament:
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=zJxyTgLjIB8"><img src="https://img.youtube.com/vi/zJxyTgLjIB8/mqdefault.jpg" /></a>
|
||||
|
||||
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.
|
||||
4
Firmware/data/config.json
Normal file
4
Firmware/data/config.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{"show_log": "true",
|
||||
"ssid": "iot",
|
||||
"password": "Rijnstraat214",
|
||||
"timezone": "CET "}
|
||||
39
Firmware/include/README
Normal file
39
Firmware/include/README
Normal file
@@ -0,0 +1,39 @@
|
||||
|
||||
This directory is intended for project header files.
|
||||
|
||||
A header file is a file containing C declarations and macro definitions
|
||||
to be shared between several project source files. You request the use of a
|
||||
header file in your project source file (C, C++, etc) located in `src` folder
|
||||
by including it, with the C preprocessing directive `#include'.
|
||||
|
||||
```src/main.c
|
||||
|
||||
#include "header.h"
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Including a header file produces the same results as copying the header file
|
||||
into each source file that needs it. Such copying would be time-consuming
|
||||
and error-prone. With a header file, the related declarations appear
|
||||
in only one place. If they need to be changed, they can be changed in one
|
||||
place, and programs that include the header file will automatically use the
|
||||
new version when next recompiled. The header file eliminates the labor of
|
||||
finding and changing all the copies as well as the risk that a failure to
|
||||
find one copy will result in inconsistencies within a program.
|
||||
|
||||
In C, the usual convention is to give header files names that end with `.h'.
|
||||
It is most portable to use only letters, digits, dashes, and underscores in
|
||||
header file names, and at most one dot.
|
||||
|
||||
Read more about using header files in official GCC documentation:
|
||||
|
||||
* Include Syntax
|
||||
* Include Operation
|
||||
* Once-Only Headers
|
||||
* Computed Includes
|
||||
|
||||
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||
58
Firmware/lib/json11/CMakeLists.txt
Normal file
58
Firmware/lib/json11/CMakeLists.txt
Normal file
@@ -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)
|
||||
19
Firmware/lib/json11/LICENSE.txt
Normal file
19
Firmware/lib/json11/LICENSE.txt
Normal file
@@ -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.
|
||||
15
Firmware/lib/json11/Makefile
Normal file
15
Firmware/lib/json11/Makefile
Normal file
@@ -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
|
||||
42
Firmware/lib/json11/README.md
Normal file
42
Firmware/lib/json11/README.md
Normal file
@@ -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<Point> 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.
|
||||
790
Firmware/lib/json11/json11.cpp
Normal file
790
Firmware/lib/json11/json11.cpp
Normal file
@@ -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 <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
|
||||
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<uint8_t>(ch) <= 0x1f) {
|
||||
char buf[8];
|
||||
snprintf(buf, sizeof buf, "\\u%04x", ch);
|
||||
out += buf;
|
||||
} else if (static_cast<uint8_t>(ch) == 0xe2 && static_cast<uint8_t>(value[i+1]) == 0x80
|
||||
&& static_cast<uint8_t>(value[i+2]) == 0xa8) {
|
||||
out += "\\u2028";
|
||||
i += 2;
|
||||
} else if (static_cast<uint8_t>(ch) == 0xe2 && static_cast<uint8_t>(value[i+1]) == 0x80
|
||||
&& static_cast<uint8_t>(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 <Json::Type tag, typename T>
|
||||
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<const Value<tag, T> *>(other)->m_value;
|
||||
}
|
||||
bool less(const JsonValue * other) const override {
|
||||
return m_value < static_cast<const Value<tag, T> *>(other)->m_value;
|
||||
}
|
||||
|
||||
const T m_value;
|
||||
void dump(string &out) const override { json11::dump(m_value, out); }
|
||||
};
|
||||
|
||||
class JsonDouble final : public Value<Json::NUMBER, double> {
|
||||
double number_value() const override { return m_value; }
|
||||
int int_value() const override { return static_cast<int>(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<Json::NUMBER, int> {
|
||||
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<Json::BOOL, bool> {
|
||||
bool bool_value() const override { return m_value; }
|
||||
public:
|
||||
explicit JsonBoolean(bool value) : Value(value) {}
|
||||
};
|
||||
|
||||
class JsonString final : public Value<Json::STRING, string> {
|
||||
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<Json::ARRAY, Json::array> {
|
||||
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<Json::OBJECT, Json::object> {
|
||||
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<Json::NUL, NullStruct> {
|
||||
public:
|
||||
JsonNull() : Value({}) {}
|
||||
};
|
||||
|
||||
/* * * * * * * * * * * * * * * * * * * *
|
||||
* Static globals - static-init-safe
|
||||
*/
|
||||
struct Statics {
|
||||
const std::shared_ptr<JsonValue> null = make_shared<JsonNull>();
|
||||
const std::shared_ptr<JsonValue> t = make_shared<JsonBoolean>(true);
|
||||
const std::shared_ptr<JsonValue> f = make_shared<JsonBoolean>(false);
|
||||
const string empty_string;
|
||||
const vector<Json> empty_vector;
|
||||
const map<string, Json> 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<JsonDouble>(value)) {}
|
||||
Json::Json(int value) : m_ptr(make_shared<JsonInt>(value)) {}
|
||||
Json::Json(bool value) : m_ptr(value ? statics().t : statics().f) {}
|
||||
Json::Json(const string &value) : m_ptr(make_shared<JsonString>(value)) {}
|
||||
Json::Json(string &&value) : m_ptr(make_shared<JsonString>(move(value))) {}
|
||||
Json::Json(const char * value) : m_ptr(make_shared<JsonString>(value)) {}
|
||||
Json::Json(const Json::array &values) : m_ptr(make_shared<JsonArray>(values)) {}
|
||||
Json::Json(Json::array &&values) : m_ptr(make_shared<JsonArray>(move(values))) {}
|
||||
Json::Json(const Json::object &values) : m_ptr(make_shared<JsonObject>(values)) {}
|
||||
Json::Json(Json::object &&values) : m_ptr(make_shared<JsonObject>(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> & Json::array_items() const { return m_ptr->array_items(); }
|
||||
const map<string, Json> & 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<Json> & JsonValue::array_items() const { return statics().empty_vector; }
|
||||
const map<string, Json> & 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<uint8_t>(c) >= 0x20 && static_cast<uint8_t>(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 <typename T>
|
||||
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<char>(0);
|
||||
if (i == str.size())
|
||||
return fail("unexpected end of input", static_cast<char>(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<char>(pt);
|
||||
} else if (pt < 0x800) {
|
||||
out += static_cast<char>((pt >> 6) | 0xC0);
|
||||
out += static_cast<char>((pt & 0x3F) | 0x80);
|
||||
} else if (pt < 0x10000) {
|
||||
out += static_cast<char>((pt >> 12) | 0xE0);
|
||||
out += static_cast<char>(((pt >> 6) & 0x3F) | 0x80);
|
||||
out += static_cast<char>((pt & 0x3F) | 0x80);
|
||||
} else {
|
||||
out += static_cast<char>((pt >> 18) | 0xF0);
|
||||
out += static_cast<char>(((pt >> 12) & 0x3F) | 0x80);
|
||||
out += static_cast<char>(((pt >> 6) & 0x3F) | 0x80);
|
||||
out += static_cast<char>((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<size_t>(std::numeric_limits<int>::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<string, Json> 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<Json> 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> 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> 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
|
||||
232
Firmware/lib/json11/json11.hpp
Normal file
232
Firmware/lib/json11/json11.hpp
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <initializer_list>
|
||||
|
||||
#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<Json> array;
|
||||
typedef std::map<std::string, Json> 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 <class T, class = decltype(&T::to_json)>
|
||||
Json(const T & t) : Json(t.to_json()) {}
|
||||
|
||||
// Implicit constructor: map-like objects (std::map, std::unordered_map, etc)
|
||||
template <class M, typename std::enable_if<
|
||||
std::is_constructible<std::string, decltype(std::declval<M>().begin()->first)>::value
|
||||
&& std::is_constructible<Json, decltype(std::declval<M>().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 <class V, typename std::enable_if<
|
||||
std::is_constructible<Json, decltype(*std::declval<V>().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<Json> parse_multi(
|
||||
const std::string & in,
|
||||
std::string::size_type & parser_stop_pos,
|
||||
std::string & err,
|
||||
JsonParse strategy = JsonParse::STANDARD);
|
||||
|
||||
static inline std::vector<Json> 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<std::pair<std::string, Type>> shape;
|
||||
bool has_shape(const shape & types, std::string & err) const;
|
||||
|
||||
private:
|
||||
std::shared_ptr<JsonValue> 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
|
||||
9
Firmware/lib/json11/json11.pc.in
Normal file
9
Firmware/lib/json11/json11.pc.in
Normal file
@@ -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}
|
||||
286
Firmware/lib/json11/test.cpp
Normal file
286
Firmware/lib/json11/test.cpp
Normal file
@@ -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 <cassert>
|
||||
#include <string>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include "json11.hpp"
|
||||
#include <list>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <algorithm>
|
||||
#include <type_traits>
|
||||
|
||||
// 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<Json>);
|
||||
CHECK_TRAIT(is_nothrow_default_constructible<Json>);
|
||||
CHECK_TRAIT(is_copy_constructible<Json>);
|
||||
CHECK_TRAIT(is_nothrow_move_constructible<Json>);
|
||||
CHECK_TRAIT(is_copy_assignable<Json>);
|
||||
CHECK_TRAIT(is_nothrow_move_assignable<Json>);
|
||||
CHECK_TRAIT(is_nothrow_destructible<Json>);
|
||||
|
||||
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<int> l1 { 1, 2, 3 };
|
||||
std::vector<int> l2 { 1, 2, 3 };
|
||||
std::set<int> l3 { 1, 2, 3 };
|
||||
JSON11_TEST_ASSERT(Json(l1) == Json(l2));
|
||||
JSON11_TEST_ASSERT(Json(l2) == Json(l3));
|
||||
|
||||
std::map<string, string> m1 { { "k1", "v1" }, { "k2", "v2" } };
|
||||
std::unordered_map<string, string> 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<string, string>{ { "k1", "v1" } })},
|
||||
{bad_json1, good_json.size() + 1, 1, Json(std::map<string, string>{ { "k1", "v1" } })},
|
||||
{bad_json2, good_json.size(), 1, Json(std::map<string, string>{ { "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<Point> 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
|
||||
56
Firmware/platformio.ini
Normal file
56
Firmware/platformio.ini
Normal file
@@ -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"
|
||||
419
Firmware/src/display_task.cpp
Normal file
419
Firmware/src/display_task.cpp
Normal file
@@ -0,0 +1,419 @@
|
||||
#include "display_task.h"
|
||||
|
||||
#include <AceButton.h>
|
||||
#include <SD_MMC.h>
|
||||
#include <TFT_eSPI.h>
|
||||
#include <Update.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include <json11.hpp>
|
||||
|
||||
#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<std::string>& 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<std::string> main_gifs;
|
||||
std::vector<std::string> 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());
|
||||
}
|
||||
48
Firmware/src/display_task.h
Normal file
48
Firmware/src/display_task.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD_MMC.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
#include "logger.h"
|
||||
#include "main_task.h"
|
||||
#include "task.h"
|
||||
|
||||
enum class State {
|
||||
CHOOSE_GIF,
|
||||
PLAY_GIF,
|
||||
SHOW_CREDITS,
|
||||
};
|
||||
|
||||
class DisplayTask : public Task<DisplayTask>, public Logger {
|
||||
friend class Task<DisplayTask>; // 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<std::string>& 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;
|
||||
|
||||
};
|
||||
22
Firmware/src/event.h
Normal file
22
Firmware/src/event.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#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;
|
||||
};
|
||||
};
|
||||
225
Firmware/src/gif_player.cpp
Normal file
225
Firmware/src/gif_player.cpp
Normal file
@@ -0,0 +1,225 @@
|
||||
#include "gif_player.h"
|
||||
|
||||
#include <AnimatedGIF.h>
|
||||
#include <SD_MMC.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
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<File *>(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<File *>(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<File *>(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;
|
||||
}
|
||||
45
Firmware/src/gif_player.h
Normal file
45
Firmware/src/gif_player.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <AnimatedGIF.h>
|
||||
#include <Arduino.h>
|
||||
#include <SD_MMC.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
#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);
|
||||
|
||||
};
|
||||
9
Firmware/src/logger.h
Normal file
9
Firmware/src/logger.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
class Logger {
|
||||
public:
|
||||
Logger() {};
|
||||
virtual ~Logger() {};
|
||||
virtual void log(const char* msg) = 0;
|
||||
|
||||
};
|
||||
21
Firmware/src/main.cpp
Normal file
21
Firmware/src/main.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
219
Firmware/src/main_task.cpp
Normal file
219
Firmware/src/main_task.cpp
Normal file
@@ -0,0 +1,219 @@
|
||||
#include "main_task.h"
|
||||
|
||||
#include <AceButton.h>
|
||||
#include <ArduinoOTA.h>
|
||||
#include <lwip/apps/sntp.h>
|
||||
#include <time.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
48
Firmware/src/main_task.h
Normal file
48
Firmware/src/main_task.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
#include <AceButton.h>
|
||||
|
||||
#include "event.h"
|
||||
#include "logger.h"
|
||||
#include "task.h"
|
||||
|
||||
class MainTask : public Task<MainTask>, public ace_button::IEventHandler {
|
||||
friend class Task<MainTask>; // 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<QueueHandle_t> event_queues_;
|
||||
};
|
||||
33
Firmware/src/semaphore_guard.h
Normal file
33
Firmware/src/semaphore_guard.h
Normal file
@@ -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 <Arduino.h>
|
||||
|
||||
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_;
|
||||
};
|
||||
54
Firmware/src/task.h
Normal file
54
Firmware/src/task.h
Normal file
@@ -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<Arduino.h>
|
||||
|
||||
// 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 T>
|
||||
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<T*>(params);
|
||||
t->run();
|
||||
}
|
||||
|
||||
const char* name;
|
||||
uint32_t stackDepth;
|
||||
UBaseType_t priority;
|
||||
TaskHandle_t taskHandle;
|
||||
const BaseType_t coreId;
|
||||
};
|
||||
11
Firmware/test/README
Normal file
11
Firmware/test/README
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user