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