diff --git a/frontend/src/components/ActivityBaseConfig.tsx b/frontend/src/components/ActivityBaseConfig.tsx index a7a757e..087b5ef 100644 --- a/frontend/src/components/ActivityBaseConfig.tsx +++ b/frontend/src/components/ActivityBaseConfig.tsx @@ -241,7 +241,7 @@ function Team(props) {
; + const eventData = useEvent(); + const endpoint = "/api/v1/soccer"; + const [data, setData] = createSignal(null); + + const halves = ["1st Half", "2nd Half"]; + + onMount(async () => { + const resp = await fetch(endpoint); + const json = await resp.json(); + + if (json != null) { + setData(json); + } + }); + + createEffect(() => { + if (eventData() != null) { + setData(eventData()); + } + }); + + return ( + + + + + + ); } export default SoccerConfig; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 846f1ad..5187527 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,6 +7,7 @@ idf_component_register(SRCS "lumen/activity/field/text.cpp" "lumen/activity/field/timer.cpp" "lumen/activity/football.cpp" + "lumen/activity/soccer.cpp" "lumen/app_task.cpp" "lumen/button_callback.cpp" "lumen/hardware/button/button.cpp" @@ -23,6 +24,7 @@ idf_component_register(SRCS "lumen/web/handler/common.cpp" "lumen/web/handler/event.cpp" "lumen/web/handler/football.cpp" + "lumen/web/handler/soccer.cpp" "lumen/web/server.cpp" "main.cpp" INCLUDE_DIRS "." diff --git a/src/lumen/activity/activity.hpp b/src/lumen/activity/activity.hpp index 60a7cb5..d4ac354 100644 --- a/src/lumen/activity/activity.hpp +++ b/src/lumen/activity/activity.hpp @@ -19,6 +19,7 @@ enum class Type { none = 0, connect, football, + soccer, }; enum class ButtonEvent { diff --git a/src/lumen/activity/context.cpp b/src/lumen/activity/context.cpp index 31a4893..c2deb54 100644 --- a/src/lumen/activity/context.cpp +++ b/src/lumen/activity/context.cpp @@ -2,6 +2,7 @@ #include "lumen/activity/connect.hpp" #include "lumen/activity/football.hpp" +#include "lumen/activity/soccer.hpp" #include "lumen/hardware/sd_card.hpp" #include "esp_log.h" @@ -71,6 +72,10 @@ void Context::set_activity(Type type) activity_ = std::move(std::make_unique()); break; + case Type::soccer: + activity_ = std::move(std::make_unique()); + break; + default: ESP_LOGW(tag, "Unknown sport"); return; diff --git a/src/lumen/activity/soccer.cpp b/src/lumen/activity/soccer.cpp new file mode 100644 index 0000000..f53c364 --- /dev/null +++ b/src/lumen/activity/soccer.cpp @@ -0,0 +1,50 @@ +#include "lumen/activity/soccer.hpp" + +using json = nlohmann::json; + +namespace lumen::activity { + +field::Team& Soccer::team_one() +{ + return team_one_; +} + +field::Team& Soccer::team_two() +{ + return team_two_; +} + +field::Number& Soccer::half() +{ + return half_; +} + +field::Timer& Soccer::timer() +{ + return timer_; +} + +void Soccer::update_display() {} + +json Soccer::to_json() +{ + return { + {"type", "soccer"}, + {"teamOne", + {{"name", team_one_.name().get_value()}, + {"score", team_one_.score().get_value()}}}, + {"teamTwo", + {{"name", team_two_.name().get_value()}, + {"score", team_two_.score().get_value()}}}, + {"half", + {{"value", half_.get_value()}, + {"startValue", half_.get_start_value()}}}, + {"timer", + {{"value", timer_.get_value()}, + {"startTime", timer_.get_start_time()}, + {"countUp", timer_.is_count_up()}, + {"isRunning", timer_.is_running()}}} + }; +} + +} // namespace lumen::activity diff --git a/src/lumen/activity/soccer.hpp b/src/lumen/activity/soccer.hpp new file mode 100644 index 0000000..b15effc --- /dev/null +++ b/src/lumen/activity/soccer.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "lumen/activity/activity.hpp" +#include "lumen/activity/field/number.hpp" +#include "lumen/activity/field/team.hpp" +#include "lumen/activity/field/timer.hpp" + +#include "nlohmann/json.hpp" + +namespace lumen::activity { + +/// Stores and displays the data for a Soccer game. +class Soccer : public Activity { +public: + /// Return the first team. + field::Team& team_one(); + + /// Return the second team. + field::Team& team_two(); + + /// Return the quarter field. + field::Number& half(); + + /// Return the timer field + field::Timer& timer(); + + /// Draw the fields to the display. + void update_display() override; + + /** Constructs a JSON object from the `Soccer` object. + * + * \returns The JSON object. + */ + nlohmann::json to_json() override; + +private: + field::Team team_one_{"Home"}; + field::Team team_two_{"Away"}; + + field::Number half_{1}; + + // Soccer games typically have 45 minute halves. + field::Timer timer_{45 * 60}; +}; + +} // namespace lumen::activity diff --git a/src/lumen/web/handler/soccer.cpp b/src/lumen/web/handler/soccer.cpp new file mode 100644 index 0000000..a069836 --- /dev/null +++ b/src/lumen/web/handler/soccer.cpp @@ -0,0 +1,151 @@ +#include "lumen/web/handler/soccer.hpp" + +#include "lumen/activity/activity.hpp" +#include "lumen/activity/soccer.hpp" +#include "lumen/web/error.hpp" +#include "lumen/web/server.hpp" + +#include "esp_log.h" +#include "nlohmann/json.hpp" + +using json = nlohmann::json; + +namespace lumen::web::handler { +namespace { + +constexpr auto tag = "web/handler/soccer"; + +constexpr auto buffer_size = 128; + +} + +esp_err_t soccer_get(httpd_req_t* request) +{ + auto* activity_context = + static_cast(httpd_get_global_user_ctx(request->handle)) + ->get_activity_context(); + + activity_context->set_activity(activity::Type::soccer); + + auto soccer_data = activity_context->get_activity_json(); + + httpd_resp_set_type(request, "application/json"); + httpd_resp_sendstr(request, soccer_data.dump().c_str()); + + return ESP_OK; +} + +esp_err_t soccer_put(httpd_req_t* request) +{ + ESP_LOGI(tag, "Soccer PUT handler"); + // Make sure the correct endpoint was hit + auto* activity_context = + static_cast(httpd_get_global_user_ctx(request->handle)) + ->get_activity_context(); + + if (activity_context->get_activity_type() != activity::Type::soccer) { + ESP_LOGW( + tag, + "Current activity is not soccer: %d", + static_cast(activity_context->get_activity_type()) + ); + + return send_error(request, 400, "The current activity is not soccer"); + } + + // Make sure we can store the request JSON in our buffer + auto const content_length = request->content_len; + + if (content_length > buffer_size - 1) { + ESP_LOGW(tag, "Received more data than can be processed"); + return send_error( + request, 500, "More data was received than can be processed" + ); + } + + char buffer[buffer_size]; + + // Read in the request JSON + int received_bytes = httpd_req_recv(request, buffer, buffer_size); + buffer[content_length] = '\0'; + + if (received_bytes <= 0) { + return send_error(request, 500, "Error reading request content"); + } + + auto const request_json = json::parse(buffer); + + ESP_LOGI(tag, "Request: %s", request_json.dump().c_str()); + + auto* soccer = + static_cast(activity_context->get_activity()); + + // Parse and validate the request JSON + + if (request_json.contains("teamOne")) { + if (request_json["teamOne"].contains("name")) { + auto name = request_json["teamOne"]["name"]; + + // Set a limit of six characters + if (name.is_string() && name.size() <= 6) { + soccer->team_one().name().set_value(name); + } + } + if (request_json["teamOne"].contains("score")) { + auto score = request_json["teamOne"]["score"]; + + if (score.is_number_unsigned()) { + soccer->team_one().score().set_value(score); + } + } + } + + if (request_json.contains("teamTwo")) { + if (request_json["teamTwo"].contains("name")) { + auto name = request_json["teamTwo"]["name"]; + + // Set a limit of six characters + if (name.is_string() && name.size() <= 6) { + soccer->team_two().name().set_value(name); + } + } + if (request_json["teamTwo"].contains("score")) { + auto score = request_json["teamTwo"]["score"]; + + if (score.is_number_unsigned()) { + soccer->team_two().score().set_value(score); + } + } + } + + if (request_json.contains("half") && + request_json["half"].is_number_unsigned()) { + soccer->half().set_value(request_json["half"]); + } + + if (request_json.contains("timer")) { + if (request_json["timer"].contains("isRunning") && + request_json["timer"]["isRunning"].is_boolean()) { + if (request_json["timer"]["isRunning"]) { + if (soccer->timer().get_value() > 0) { + soccer->timer().start(); + } else { + soccer->timer().reset(); + soccer->timer().start(); + } + } else { + soccer->timer().stop(); + } + } + + if (request_json["timer"].contains("value") && + request_json["timer"]["value"].is_number_unsigned()) { + soccer->timer().set_value(request_json["timer"]["value"]); + } + } + + httpd_resp_send(request, nullptr, 0); + return ESP_OK; +} + +} // namespace lumen::web::handler diff --git a/src/lumen/web/handler/soccer.hpp b/src/lumen/web/handler/soccer.hpp new file mode 100644 index 0000000..24c9322 --- /dev/null +++ b/src/lumen/web/handler/soccer.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "esp_http_server.h" + +namespace lumen::web::handler { + +/** The GET handler for the soccer endpoint. + * + * \param request Valid pointer to the request being handled. + */ +esp_err_t soccer_get(httpd_req_t* request); + +/** The PUT handler for the soccer endpoint. + * + * \param request Valid pointer to the request being handled. + */ +esp_err_t soccer_put(httpd_req_t* request); + +} // namespace lumen::web::handler diff --git a/src/lumen/web/server.cpp b/src/lumen/web/server.cpp index 319bbec..93cc2c9 100644 --- a/src/lumen/web/server.cpp +++ b/src/lumen/web/server.cpp @@ -5,6 +5,7 @@ #include "lumen/web/handler/common.hpp" #include "lumen/web/handler/event.hpp" #include "lumen/web/handler/football.hpp" +#include "lumen/web/handler/soccer.hpp" #include "esp_log.h" #include "freertos/FreeRTOS.h" @@ -71,6 +72,7 @@ Server::Server(activity::Context& activity_context) start_event_stream_task(); config_.stack_size = 8192; + config_.max_uri_handlers = 16; config_.global_user_ctx = this; config_.uri_match_fn = httpd_uri_match_wildcard; @@ -159,6 +161,20 @@ void register_endpoints(httpd_handle_t& server) .user_ctx = nullptr }; + httpd_uri_t soccer_get_uri = { + .uri = "/api/v1/soccer", + .method = HTTP_GET, + .handler = handler::soccer_get, + .user_ctx = nullptr + }; + + httpd_uri_t soccer_put_uri = { + .uri = "/api/v1/soccer", + .method = HTTP_PUT, + .handler = handler::soccer_put, + .user_ctx = nullptr + }; + httpd_uri_t advertisement_get_uri = { .uri = "/api/v1/advertisement", .method = HTTP_GET, @@ -190,6 +206,8 @@ void register_endpoints(httpd_handle_t& server) httpd_register_uri_handler(server, &event_get_uri); httpd_register_uri_handler(server, &football_get_uri); httpd_register_uri_handler(server, &football_put_uri); + httpd_register_uri_handler(server, &soccer_get_uri); + httpd_register_uri_handler(server, &soccer_put_uri); httpd_register_uri_handler(server, &advertisement_get_uri); httpd_register_uri_handler(server, &advertisement_post_uri); httpd_register_uri_handler(server, &advertisement_delete_uri);