diff --git a/rgboard/Makefile b/rgboard/Makefile new file mode 100644 index 000000000..3aab6983b --- /dev/null +++ b/rgboard/Makefile @@ -0,0 +1,35 @@ +# Compiler settings +CXX = g++ +CXXFLAGS = -Wall -O3 -g -Wextra -Wno-unused-parameter + +# Source files and executable +SRCS = src/main.cpp src/display.cpp src/queue-client.cpp +OBJECTS = $(SRCS:.cpp=.o) +BINARIES = rgboard + +# Where our RGB library resides +RGB_LIB_DISTRIBUTION = .. +RGB_INCDIR = $(RGB_LIB_DISTRIBUTION)/include +RGB_LIBDIR = $(RGB_LIB_DISTRIBUTION)/lib +RGB_LIBRARY_NAME = rgbmatrix +RGB_LIBRARY = $(RGB_LIBDIR)/lib$(RGB_LIBRARY_NAME).a + +# Linker flags: add libcurl and jsoncpp +LDFLAGS += -L$(RGB_LIBDIR) -l$(RGB_LIBRARY_NAME) -lcurl -ljsoncpp -lrt -lm -lpthread + +# Default target +all: $(BINARIES) + +# Compile .o files +src/%.o: src/%.cpp + $(CXX) $(CXXFLAGS) -Iinclude -I$(RGB_INCDIR) -c -o $@ $< + +# Link final binary +rgboard: $(OBJECTS) $(RGB_LIBRARY) + $(CXX) $(CXXFLAGS) -o $@ $(OBJECTS) $(LDFLAGS) + +# Clean up +clean: + rm -f src/*.o $(BINARIES) + +.PHONY: all clean diff --git a/rgboard/include/display.h b/rgboard/include/display.h new file mode 100644 index 000000000..e217f2a42 --- /dev/null +++ b/rgboard/include/display.h @@ -0,0 +1,15 @@ +// +// Created by evalentin on 09/03/25. +// + +#ifndef DISPLAY_H +#define DISPLAY_H + +#include "led-matrix.h" + +#include // JSON parsing + +using rgb_matrix::RGBMatrix; +using rgb_matrix::Canvas; +void DrawDesignOnCanvas(Canvas* canvas, const Json::Value& pixel_data); +#endif //DISPLAY_H diff --git a/rgboard/include/queue-client.h b/rgboard/include/queue-client.h new file mode 100644 index 000000000..beb9c1c6b --- /dev/null +++ b/rgboard/include/queue-client.h @@ -0,0 +1,30 @@ +// +// Created by evalentin on 01/05/25. +// + +#ifndef QUEUE_CLIENT_H +#define QUEUE_CLIENT_H +#include +#include + +class QueueClient +{ +public: + QueueClient(std::string email, std::string password); + bool GetDesign(); + [[nodiscard]] int GetDisplayDuration() const; + Json::Value GetPixelData(); + +private: + std::string jwt; + std::string email; + std::string password; + + // We store in these variables and get from here. Can't return a string and an int in the same function. + int display_duration{}; + Json::Value pixel_data; + + std::string GetJWT(); + static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, std::string* output); +}; +#endif //QUEUE_CLIENT_H diff --git a/rgboard/src/display.cpp b/rgboard/src/display.cpp new file mode 100644 index 000000000..9770faf07 --- /dev/null +++ b/rgboard/src/display.cpp @@ -0,0 +1,58 @@ +// +// Created by evalentin on 09/03/25. +// + +#include "../include/display.h" +#include // std::string +#include // std::stringstream, std::istringstream +#include // std::cerr, std::cout + + +using rgb_matrix::RGBMatrix; +using rgb_matrix::Canvas; + +void HexToRGB(const std::string& hex, int& r, int& g, int& b); + +void DrawDesignOnCanvas(Canvas* canvas, const Json::Value& pixel_data) +{ + constexpr int GRID_SIZE = 8; // pixel data is 8x scaled + canvas->Clear(); + + for (const auto& key : pixel_data.getMemberNames()) + { + int x = 0, y = 0; + char comma; + + std::stringstream coord_stream(key); + coord_stream >> x >> comma >> y; + + // downscale + x /= GRID_SIZE; + y /= GRID_SIZE; + + const std::string hex_color = pixel_data[key].asString(); + int r, g, b; + HexToRGB(hex_color, r, g, b); + + if (x >= 0 && x < canvas->width() && y >= 0 && y < canvas->height()) + { + canvas->SetPixel(x, y, r, g, b); + } + } +} + + +// Convert hex string to RGB integers +void HexToRGB(const std::string& hex, int& r, int& g, int& b) +{ + if (hex.length() == 7 && hex[0] == '#') + { + r = std::stoi(hex.substr(1, 2), nullptr, 16); + g = std::stoi(hex.substr(3, 2), nullptr, 16); + b = std::stoi(hex.substr(5, 2), nullptr, 16); + } + else + { + r = g = b = 0; // fallback to black + } +} diff --git a/rgboard/src/main.cpp b/rgboard/src/main.cpp new file mode 100644 index 000000000..9206aec87 --- /dev/null +++ b/rgboard/src/main.cpp @@ -0,0 +1,60 @@ +// +// Created by evalentin on 08/03/25. +// + +#define ROWS 32 +#define COLS 64 +#define PARALLEL 2 +#define HARDWARE "regular" +#define GPIO_SLOWDOWN 4 + +#include + +#include "queue-client.h" +#include "display.h" + +int main(int argc, char* argv[]) +{ + // Matrix configuration + rgb_matrix::RGBMatrix::Options defaults; + rgb_matrix::RuntimeOptions runtime_options; + + defaults.hardware_mapping = HARDWARE; + defaults.rows = ROWS; + defaults.cols = COLS; + defaults.parallel = PARALLEL; + + runtime_options.gpio_slowdown = GPIO_SLOWDOWN; + + // Initialize matrix + rgb_matrix::Canvas* canvas = rgb_matrix::CreateMatrixFromOptions(defaults, runtime_options); + if (canvas == nullptr) + { + std::fprintf(stderr, "Failed to initialize matrix.\n"); + return 1; + } + + // Authenticate with client + QueueClient client("email", "password"); + + // main loop: fetch + draw + wait + while (true) + { + if (client.GetDesign()) + { + const Json::Value& pixel_data = client.GetPixelData(); + int duration = client.GetDisplayDuration(); + + DrawDesignOnCanvas(canvas, pixel_data); + std::printf("Displaying design for %d seconds.\n", duration); + sleep(duration); + } + else + { + std::fprintf(stderr, "Failed to get design. Retrying in 5 seconds...\n"); + sleep(5); + } + } + + return 0; +} diff --git a/rgboard/src/queue-client.cpp b/rgboard/src/queue-client.cpp new file mode 100644 index 000000000..0e52a201a --- /dev/null +++ b/rgboard/src/queue-client.cpp @@ -0,0 +1,224 @@ +// +// Created by evalentin on 01/05/25. +// + +#include "../include/queue-client.h" +#include +#include + +#define LOGIN_URL "http://localhost:5000/login" +#define CURRENT_URL "http://localhost:5000/rotation/current" + + +QueueClient::QueueClient(std::string email, std::string password) +{ + this->email = std::move(email); + this->password = std::move(password); + jwt = GetJWT(); +} + + +std::string QueueClient::GetJWT() +{ + if (this->email.empty()) + { + std::printf("Email field is empty.\n"); + return ""; + } + else if (password.empty()) + { + std::printf("Password field is empty.\n"); + return ""; + } + + CURL* curl = curl_easy_init(); + + if (!curl) + { + printf("Curl init failed\n"); + return ""; + } + + std::string response; + Json::Value body; + + // Doing this in C++ is crazy, even the IDE is confused. + body["email"] = this->email; + body["password"] = this->password; + + Json::StreamWriterBuilder writer; + + std::string json_body = Json::writeString(writer, body); + + struct curl_slist* headers = nullptr; + + headers = curl_slist_append(headers, "Content-Type: application/json"); + + // Set options + curl_easy_setopt(curl, CURLOPT_URL, LOGIN_URL); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + // Send request + CURLcode result = curl_easy_perform(curl); + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (result != CURLE_OK) + { + fprintf(stderr, "problem: %s\n", curl_easy_strerror(result)); + return ""; + } + + Json::CharReaderBuilder reader; + Json::Value json_response; + std::string errs; + + std::istringstream s(response); + + if (!Json::parseFromStream(reader, s, &json_response, &errs)) + { + printf("Failed to parse response JSON: %s\n", errs.c_str()); + return ""; + } + + if (json_response.isMember("access_token")) + { + std::string token = json_response["access_token"].asString(); + return token; + } + + std::string error_message = json_response["error"].asString(); + printf("Login failed: no token in response.\nError: %s\n", error_message.c_str()); + return ""; +} + +bool QueueClient::GetDesign() +{ + auto perform_request = [this](std::string& response) -> long + { + CURL* curl = curl_easy_init(); + if (!curl) + { + std::printf("Curl init failed\n"); + return -1; + } + + struct curl_slist* headers = nullptr; + std::string auth_header = "Authorization: Bearer " + this->jwt; + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, auth_header.c_str()); + + curl_easy_setopt(curl, CURLOPT_URL, CURRENT_URL); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); // use GET + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + CURLcode result = curl_easy_perform(curl); + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (result != CURLE_OK) + { + std::fprintf(stderr, "Request failed: %s\n", curl_easy_strerror(result)); + return -1; + } + + return http_code; + }; + + // First attempt + std::string response; + long status_code = perform_request(response); + + // If unauthorized, refresh token and try again + if (status_code == 401) + { + std::printf("JWT expired or invalid. Getting new token...\n"); + this->jwt = GetJWT(); + if (this->jwt.empty()) + { + std::printf("Failed to refresh JWT.\n"); + return false; + } + + response.clear(); + status_code = perform_request(response); + } + + if (status_code != 200) + { + std::printf("Server responded with HTTP %ld\n", status_code); + return false; + } + + // Parse JSON response + Json::CharReaderBuilder reader; + Json::Value json_response; + std::string errs; + std::istringstream s(response); + + if (!Json::parseFromStream(reader, s, &json_response, &errs)) + { + std::printf("Failed to parse response JSON: %s\n", errs.c_str()); + return false; + } + + if (json_response.isMember("image") && json_response["image"].isMember("pixel_data") && json_response. + isMember("time_left")) + { + std::string raw = json_response["image"]["pixel_data"].asString(); + + // Strip "Design " prefix if present + const std::string prefix = "Design "; + if (raw.rfind(prefix, 0) == 0) + raw = raw.substr(prefix.length()); + + Json::CharReaderBuilder builder; + Json::Value parsed; + std::string errs; + std::istringstream ss(raw); + + if (!Json::parseFromStream(builder, ss, &parsed, &errs)) + { + std::fprintf(stderr, "Failed to parse pixel_data string: %s\n", errs.c_str()); + return false; + } + + this->pixel_data = parsed; + this->display_duration = json_response["time_left"].asInt(); + return true; + } + + std::string error_message = json_response.get("error", "Unknown error").asString(); + std::printf("Failed to get design: %s\n", error_message.c_str()); + return false; +} + + +size_t QueueClient::CurlWriteCallback(void* contents, size_t size, size_t nmemb, std::string* output) +{ + size_t total = size * nmemb; + output->append((char*)contents, total); + return total; +} + + +// Getters + +int QueueClient::GetDisplayDuration() const +{ + return this->display_duration; +} + +Json::Value QueueClient::GetPixelData() +{ + return this->pixel_data; +}