added code from gh

This commit is contained in:
2023-12-23 20:37:34 +01:00
parent 675fda89a1
commit 757ad51899
27 changed files with 2932 additions and 0 deletions

BIN
.DS_Store vendored

Binary file not shown.

20
Firmware/.gitignore vendored Normal file
View 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
View 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
View 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.

View File

@@ -0,0 +1,4 @@
{"show_log": "true",
"ssid": "iot",
"password": "Rijnstraat214",
"timezone": "CET "}

39
Firmware/include/README Normal file
View 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

View 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)

View 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.

View 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

View 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.

View 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

View 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

View 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}

View 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
View 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"

View 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());
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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, &notify_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
View 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_;
};

View 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
View 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
View 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