diff --git a/CMakeLists.txt b/CMakeLists.txt index 108ced6..cd4a344 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.16) include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +idf_build_set_property(MINIMAL_BUILD ON) + project(Lumen) set(${IDF_TARGET} esp32c3) diff --git a/assets/icon_minecraft_sync.png b/assets/icon_minecraft_sync.png new file mode 100644 index 0000000..1c245c8 Binary files /dev/null and b/assets/icon_minecraft_sync.png differ diff --git a/components/vision-ui/libvision_ui.a b/components/vision-ui/libvision_ui.a index 5d9331a..77d731b 100644 Binary files a/components/vision-ui/libvision_ui.a and b/components/vision-ui/libvision_ui.a differ diff --git a/components/vision-ui/vision_ui_lib.h b/components/vision-ui/vision_ui_lib.h index c572586..2d065c1 100644 --- a/components/vision-ui/vision_ui_lib.h +++ b/components/vision-ui/vision_ui_lib.h @@ -103,13 +103,13 @@ typedef struct vision_ui_list_icon { // NOLINT size_t footer_width; // NOLINT size_t footer_height; // NOLINT -} vision_ui_icon_t; // NOLINT +} vision_ui_icon_t; typedef struct vision_ui_font { // NOLINT const void* font; // NOLINT int8_t top_compensation; // NOLINT int8_t bottom_compensation; // NOLINT -} vision_ui_font_t; // NOLINT +} vision_ui_font_t; typedef enum vision_ui_action_t { // NOLINT UiActionNone, @@ -117,7 +117,7 @@ typedef enum vision_ui_action_t { // NOLINT UiActionGoNext, UiActionEnter, UiActionExit, -} vision_ui_action_t; // NOLINT +} vision_ui_action_t; #else #include "vision_ui_item.h" #include "vision_ui_renderer.h" @@ -139,9 +139,9 @@ typedef struct LumenSystemConfig { const uint8_t* usbIcon; const uint8_t* statIcon; const uint8_t* creeperIcon; + const uint8_t* minecraftSyncIcon; } LumenSystemConfig; - LumenSystemInfo lumenGetSystemInfo(); LumenSystemConfig lumenGetSystemConfig(); @@ -183,6 +183,14 @@ typedef struct LumenEasterEggState { LumenEasterEgg lumenGetEasterEgg(); LumenEasterEggState lumenGetEasterEggState(); +typedef struct LumenMinecraftSync { + void (*initFunction)(); + void (*loopFunction)(); + void (*exitFunction)(); +} LumenMinecraftSync; + +LumenMinecraftSync lumenGetMinecraftSync(); + extern void vision_ui_step_render(); // NOLINT extern void vision_ui_allocator_set(void* (*allocator)(vision_alloc_op_t op, size_t size, size_t count, void* ptr) ); // NOLINT diff --git a/dependencies.lock b/dependencies.lock index 6094fd2..f872ae1 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -1,6 +1,6 @@ dependencies: espp/base_component: - component_hash: a4da2e51f8545f3f56c2cc4df62d58a5c1683aae6dca1303da6eeb382735d916 + component_hash: 569d8a4c2520fbb8f9c79ddd772e278506c80099e218599c602a8891bb995bf9 dependencies: - name: espp/logger registry_url: https://components.espressif.com @@ -12,9 +12,9 @@ dependencies: source: registry_url: https://components.espressif.com type: service - version: 1.0.31 + version: 1.0.33 espp/base_peripheral: - component_hash: a4c8029460ae556c256feb44d371bb6d9a47b9d0e7f862abf8ea2a312490b46e + component_hash: e9d1f27fa9346541b76aebb7dbf6150284cc600ac8019e208f600f6afac49990 dependencies: - name: espp/base_component registry_url: https://components.espressif.com @@ -26,7 +26,7 @@ dependencies: source: registry_url: https://components.espressif.com type: service - version: 1.0.31 + version: 1.0.33 espp/filters: component_hash: 5f680ee0759e546157ecf3140dcb22122cf27136acc745e3e404f4a9cdf01dbc dependencies: @@ -50,7 +50,7 @@ dependencies: type: service version: 1.0.31 espp/format: - component_hash: 97ebfaceff6189ab95190bee4da489c48e5295df8ddfcd401eabae88fead8bb8 + component_hash: 9723eeb2cadb7191255337320f51edec7b300c648d98aa4ab92e93365297acc4 dependencies: - name: idf require: private @@ -58,9 +58,9 @@ dependencies: source: registry_url: https://components.espressif.com type: service - version: 1.0.31 + version: 1.0.33 espp/logger: - component_hash: 4a0c402511864c995d9261642fb545c1af2a3fb6ff994007ce88f08155595b88 + component_hash: 91bc16342ba44200df5b56977270921a863e3447d3c94049a75e30b8d515b34d dependencies: - name: espp/format registry_url: https://components.espressif.com @@ -72,7 +72,7 @@ dependencies: source: registry_url: https://components.espressif.com type: service - version: 1.0.31 + version: 1.0.33 espp/lsm6dso: component_hash: 9fe231a62fa2d7181ae9d42f37f3929c7c42c6c8b010ba7164d97bff88fe357a dependencies: @@ -92,7 +92,7 @@ dependencies: type: service version: 1.0.31 espp/math: - component_hash: f44d40df31c2090ac56ce8e13073192563eeae259b6feb8f77084f39db1a9b29 + component_hash: e17d2f738174843a4e0ccc6d45c161e56077643f063484f184f645e469a52faa dependencies: - name: espp/format registry_url: https://components.espressif.com @@ -104,7 +104,17 @@ dependencies: source: registry_url: https://components.espressif.com type: service - version: 1.0.31 + version: 1.0.33 + espressif/cjson: + component_hash: 9372811fb197926f522c467627cf4a8e72b681e0366e17879631da801103aef3 + dependencies: + - name: idf + require: private + version: '>=5.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.7.19 espressif/esp-dsp: component_hash: 42dce32d46ac93dc11f60d368e29a830e9661c7345d794b8a45c343479cae636 dependencies: @@ -122,7 +132,8 @@ dependencies: direct_dependencies: - espp/filters - espp/lsm6dso +- espressif/cjson - idf -manifest_hash: 6cdf6f27fb1588c9d3cefad60614ba74add9b45ba0cbbc9dcfa9f50584405828 +manifest_hash: aecbf1075f1692d244f2dae5ffccd5f7ba41d52f3346cb6ed99ec47b8e40c136 target: esp32c3 version: 2.0.0 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index c7597db..ec7ca08 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -13,6 +13,7 @@ idf_component_register( esp_driver_i2c esp_driver_spi esp_lcd + cjson ina226 ) diff --git a/main/idf_component.yml b/main/idf_component.yml index eda4d74..ceaabdb 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -16,3 +16,4 @@ dependencies: # public: true espp/lsm6dso: ^1.0.31 espp/filters: ^1.0.31 + espressif/cjson: ^1.7.19 diff --git a/main/include/display.hpp b/main/include/display.hpp index 02fbddf..d1528ab 100644 --- a/main/include/display.hpp +++ b/main/include/display.hpp @@ -22,9 +22,20 @@ along with this program. If not, see . #include +#define LCD_H_RES 240 +#define LCD_V_RES 240 + extern void displayFrameRender(); extern void displayInit(vision_ui_action_t (*callback)()); +extern void displayDriverExtensionRGBBitmapDraw( + int16_t x, + int16_t y, + int16_t width, + int16_t height, + const uint16_t* colorData +); + #endif // MAIN_INCLUDE_DISPLAY_HPP diff --git a/main/include/serial_pack.hpp b/main/include/serial_pack.hpp new file mode 100644 index 0000000..371c629 --- /dev/null +++ b/main/include/serial_pack.hpp @@ -0,0 +1,38 @@ +/* +Lumen +Copyright (C) 2025 Finn Sheng + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +#pragma once + +#ifndef MAIN_INCLUDE_SERIAL_PACK_HPP +#define MAIN_INCLUDE_SERIAL_PACK_HPP + +#include +#include + +using SerialPackHandler = void (*)(const uint8_t* data, size_t currentSize); + +// Initialize USB Serial/TAG driver and internal handler table. Safe to call multiple times. +extern void serialPackInit(); +// Start the FreeRTOS task that parses incoming serial packs. +extern void serialPackStart(); +// Stop the parsing task (driver remains initialized). +extern void serialPackStop(); + +// Register or replace a handler for a given path. Call after init and before start. +extern void serialPackAttachHandler(const char* path, SerialPackHandler handler); + +#endif // MAIN_INCLUDE_SERIAL_PACK_HPP diff --git a/main/src/serial_pack.cpp b/main/src/serial_pack.cpp new file mode 100644 index 0000000..b8c8d1d --- /dev/null +++ b/main/src/serial_pack.cpp @@ -0,0 +1,293 @@ +/* +Lumen +Copyright (C) 2025 Finn Sheng + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "include/serial_pack.hpp" + +#include + +#include +#include + +#include +#include +#include +#include + + +constexpr char SERIAL_PACK_TAG[] = "[lumen:serial_pack]"; +constexpr size_t K_MAX_HANDLERS = 2; +constexpr size_t K_MAX_PATH_LEN = 16; +constexpr size_t K_MAX_DATA_LEN = 1024 * 2; +constexpr int64_t K_RX_TIMEOUT_US = 3 * 1000 * 1000; + +struct HandlerEntry { + char path[K_MAX_PATH_LEN]; + SerialPackHandler handler; +}; + +HandlerEntry S_HANDLERS[K_MAX_HANDLERS] = {}; +size_t S_HANDLER_COUNT = 0; + +TaskHandle_t S_SERIAL_TASK = nullptr; +volatile bool S_RUNNING = false; +bool S_INITIALIZED = false; + +void logUnhandledData(const char* path, const uint8_t* data, const size_t len, const bool truncated) { + char hexPreview[3 * 16 + 1] = {}; + const size_t previewLen = len > 16 ? 16 : len; + for (size_t i = 0; i < previewLen; ++i) { + static constexpr char kHex[] = "0123456789ABCDEF"; + hexPreview[i * 3] = kHex[(data[i] >> 4) & 0x0F]; + hexPreview[i * 3 + 1] = kHex[data[i] & 0x0F]; + hexPreview[i * 3 + 2] = (i + 1 < previewLen) ? ' ' : '\0'; + } + ESP_LOGW( + SERIAL_PACK_TAG, + "unhandled path '%s', size=%u, data=%s%s", + path, + static_cast(len), + hexPreview, + truncated ? " ..." : "" + ); +} + +SerialPackHandler findHandler(const char* path) { + for (size_t i = 0; i < S_HANDLER_COUNT; ++i) { + if (strcmp(S_HANDLERS[i].path, path) == 0) { + return S_HANDLERS[i].handler; + } + } + return nullptr; +} + +void resetState(char* path, size_t& pathLen, uint8_t* data, size_t& dataLen, bool& inData) { + path[0] = '\0'; + pathLen = 0; + dataLen = 0; + inData = false; +} + +void logUnhandledPath(const char* path, const uint8_t* data, const size_t len, const bool truncated) { + if (len > 0) { + logUnhandledData(path, data, len, truncated); + } else { + ESP_LOGW(SERIAL_PACK_TAG, "unhandled path '%s', size=0", path); + } +} + +[[noreturn]] +void serialPackTask(void*) { + static char path[K_MAX_PATH_LEN] = {}; + size_t pathLen = 0; + static uint8_t data[K_MAX_DATA_LEN] = {}; + size_t dataLen = 0; + bool inData = false; + bool discardUntilNewline = false; + uint8_t sizeBytes[sizeof(uint32_t)] = {}; + size_t sizeIndex = 0; + uint32_t remaining = 0; + int64_t lastRxUs = esp_timer_get_time(); + + resetState(path, pathLen, data, dataLen, inData); + + auto handleByte = [&](const uint8_t byte) { + if (discardUntilNewline) { + if (byte == '\n') { + discardUntilNewline = false; + resetState(path, pathLen, data, dataLen, inData); + } + return; + } + + if (!inData) { + if (byte == '\r') { + return; + } + if (byte == '\n') { + if (pathLen == 0) { + return; + } + path[pathLen] = '\0'; + inData = true; + dataLen = 0; + sizeIndex = 0; + remaining = 0; + ESP_LOGD(SERIAL_PACK_TAG, "path is %s", path); + return; + } + + if (byte == ' ') { + ESP_LOGE(SERIAL_PACK_TAG, "invalid path: contains space"); + discardUntilNewline = true; + return; + } + + if (pathLen + 1 >= K_MAX_PATH_LEN) { + ESP_LOGE(SERIAL_PACK_TAG, "path too long"); + discardUntilNewline = true; + return; + } + + path[pathLen++] = static_cast(byte); + return; + } + + if (sizeIndex < sizeof(uint32_t)) { + sizeBytes[sizeIndex++] = byte; + if (sizeIndex < sizeof(uint32_t)) { + return; + } + + const uint32_t size = static_cast(sizeBytes[0]) | (static_cast(sizeBytes[1]) << 8U) | + (static_cast(sizeBytes[2]) << 16U) | + (static_cast(sizeBytes[3]) << 24U); + remaining = size; + + if (remaining == 0) { + if (const SerialPackHandler handler = findHandler(path)) { + handler(nullptr, 0); + } else { + logUnhandledPath(path, nullptr, 0, false); + } + resetState(path, pathLen, data, dataLen, inData); + } + return; + } + + data[dataLen++] = byte; + if (remaining > 0) { + --remaining; + } + + if (dataLen >= K_MAX_DATA_LEN || remaining == 0) { + if (const SerialPackHandler handler = findHandler(path)) { + if (dataLen > 0) { + handler(data, dataLen); + } + } else { + logUnhandledData(path, data, dataLen, remaining > 0); + } + dataLen = 0; + } + + if (remaining == 0) { + if (const SerialPackHandler handler = findHandler(path)) { + handler(nullptr, 0); + } else { + logUnhandledPath(path, nullptr, 0, false); + } + resetState(path, pathLen, data, dataLen, inData); + } + }; + + static uint8_t rx[128] = {}; + while (S_RUNNING) { + const int read = usb_serial_jtag_read_bytes(rx, sizeof(rx), pdMS_TO_TICKS(20)); + if (!S_RUNNING) { + break; + } + if (read <= 0) { + if (inData && sizeIndex >= sizeof(uint32_t) && remaining > 0) { + const int64_t now = esp_timer_get_time(); + if (now - lastRxUs > K_RX_TIMEOUT_US) { + ESP_LOGW(SERIAL_PACK_TAG, "rx timeout, aborting pack"); + resetState(path, pathLen, data, dataLen, inData); + discardUntilNewline = false; + sizeIndex = 0; + remaining = 0; + lastRxUs = now; + } + } + continue; + } + lastRxUs = esp_timer_get_time(); + for (int i = 0; i < read; ++i) { + handleByte(rx[i]); + } + } + + S_SERIAL_TASK = nullptr; + vTaskDelete(nullptr); + while (true) { + } +} + + +void serialPackInit() { + if (S_INITIALIZED) { + return; + } + + usb_serial_jtag_driver_config_t cfg = { + .tx_buffer_size = 1024, + .rx_buffer_size = 1024 * 16, + }; + if (const esp_err_t err = usb_serial_jtag_driver_install(&cfg); err != ESP_OK) { + ESP_LOGE(SERIAL_PACK_TAG, "usb_serial_jtag_driver_install failed: %s", esp_err_to_name(err)); + return; + } + + S_INITIALIZED = true; +} + +void serialPackStart() { + if (!S_INITIALIZED) { + serialPackInit(); + } + + if (!S_INITIALIZED || S_RUNNING) { + return; + } + + S_RUNNING = true; + xTaskCreate(serialPackTask, "serial_pack", 1024 * 2, nullptr, 6, &S_SERIAL_TASK); +} + +void serialPackStop() { + if (!S_RUNNING) { + return; + } + + S_RUNNING = false; +} + +void serialPackAttachHandler(const char* path, const SerialPackHandler handler) { + if (!path || !handler) { + ESP_LOGE(SERIAL_PACK_TAG, "invalid handler registration"); + return; + } + + for (size_t i = 0; i < S_HANDLER_COUNT; ++i) { + if (strcmp(S_HANDLERS[i].path, path) == 0) { + S_HANDLERS[i].handler = handler; + return; + } + } + + if (S_HANDLER_COUNT >= K_MAX_HANDLERS) { + ESP_LOGE(SERIAL_PACK_TAG, "handler table full"); + return; + } + + ESP_LOGI(SERIAL_PACK_TAG, "handler %s is attached", path); + + std::strncpy(S_HANDLERS[S_HANDLER_COUNT].path, path, K_MAX_PATH_LEN - 1); + S_HANDLERS[S_HANDLER_COUNT].path[K_MAX_PATH_LEN - 1] = '\0'; + S_HANDLERS[S_HANDLER_COUNT].handler = handler; + ++S_HANDLER_COUNT; +} diff --git a/main/src/ui_assets_provider.cpp b/main/src/ui_assets_provider.cpp index 996e433..2743d63 100644 --- a/main/src/ui_assets_provider.cpp +++ b/main/src/ui_assets_provider.cpp @@ -1,18 +1,25 @@ #include +#include +#include #include #include #include +#include #include +#include + #include #include #include "include/buzzer.hpp" #include "include/current_sensor.hpp" +#include "include/display.hpp" #include "include/efuse.hpp" #include "include/motion.hpp" +#include "include/serial_pack.hpp" // 'logo', 240x240px // 'logo', 240x240px @@ -800,6 +807,82 @@ static uint8_t CREEPER_ICON[] = { 0x00, 0x00, 0x00, 0x00, }; +static uint8_t MINECRAFT_SYNC_ICON[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xF8, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, + 0x07, 0x80, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x01, 0x00, 0xFF, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0xFC, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, + 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, + 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, + 0x01, 0x00, 0xFC, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x07, 0x00, 0xFF, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0xC0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xF0, 0x3F, 0xF0, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFC, + 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, + 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, + 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, + 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + constexpr uint8_t U8G2_FONT_SFCOMPACT_DISPLAY_SEMIBOLD_42_ASCII[5355] U8G2_FONT_SECTION( "u8g2_font_sfcompact_display_semibold_42_ascii" ) = "_\0\5\5\6\6\4\7\7\63\66\376\365'\365)\367\7J\16k\24\316 \6\0\200\300%!\27\10" @@ -1287,6 +1370,7 @@ LumenSystemConfig lumenGetSystemConfig() { .usbIcon = USB_ICON, .statIcon = STAT_ICON, .creeperIcon = CREEPER_ICON, + .minecraftSyncIcon = MINECRAFT_SYNC_ICON, }; return config; } @@ -1924,3 +2008,193 @@ LumenEasterEggState lumenGetEasterEggState() { .ignite = igniteState, }; } + +namespace { + constexpr size_t K_MINECRAFT_SYNC_JSON_MAX = 1024; + constexpr size_t K_MINECRAFT_SYNC_SKIN_MAX = LCD_H_RES * LCD_V_RES * 2 + 4; + + struct MinecraftSyncState { + std::string mode = "-----"; + float health = 0.0F; + float maxHealth = 0.0F; + bool hasState = false; + bool serialAttached = false; + bool jsonOverflow = false; + bool skinOverflow = false; + uint16_t skinWidth = 0; + uint16_t skinHeight = 0; + bool skinReady = false; + std::vector jsonBuffer; + std::vector skinBuffer; + std::vector skinPixels; + }; + + MinecraftSyncState S_MINECRAFT_SYNC; + + uint16_t readU16Le(const uint8_t* data) { + return static_cast(data[0] | (static_cast(data[1]) << 8)); + } + + float parseJsonFloat(const cJSON* item, const float fallback) { + if (cJSON_IsNumber(item)) { + return static_cast(item->valuedouble); + } + if (cJSON_IsString(item) && item->valuestring) { + return static_cast(std::strtof(item->valuestring, nullptr)); + } + return fallback; + } + + void minecraftSyncJsonHandler(const uint8_t* data, const size_t size) { + if (data && size > 0) { + if (S_MINECRAFT_SYNC.jsonBuffer.size() + size > K_MINECRAFT_SYNC_JSON_MAX) { + S_MINECRAFT_SYNC.jsonOverflow = true; + S_MINECRAFT_SYNC.jsonBuffer.clear(); + return; + } + S_MINECRAFT_SYNC.jsonBuffer.insert(S_MINECRAFT_SYNC.jsonBuffer.end(), data, data + size); + return; + } + + if (S_MINECRAFT_SYNC.jsonOverflow) { + S_MINECRAFT_SYNC.jsonOverflow = false; + return; + } + + if (S_MINECRAFT_SYNC.jsonBuffer.empty()) { + return; + } + + cJSON* root = cJSON_ParseWithLength( + reinterpret_cast(S_MINECRAFT_SYNC.jsonBuffer.data()), S_MINECRAFT_SYNC.jsonBuffer.size() + ); + S_MINECRAFT_SYNC.jsonBuffer.clear(); + if (!root) { + return; + } + + if (const cJSON* mode = cJSON_GetObjectItem(root, "mode"); cJSON_IsString(mode) && mode->valuestring) { + S_MINECRAFT_SYNC.mode = mode->valuestring; + } + + S_MINECRAFT_SYNC.health = parseJsonFloat(cJSON_GetObjectItem(root, "health"), S_MINECRAFT_SYNC.health); + S_MINECRAFT_SYNC.maxHealth = + parseJsonFloat(cJSON_GetObjectItem(root, "max_health"), S_MINECRAFT_SYNC.maxHealth); + S_MINECRAFT_SYNC.hasState = true; + + cJSON_Delete(root); + } + + void minecraftSyncSkinHandler(const uint8_t* data, const size_t size) { + if (data && size > 0) { + if (S_MINECRAFT_SYNC.skinReady) { + S_MINECRAFT_SYNC.skinReady = false; + S_MINECRAFT_SYNC.skinBuffer.clear(); + } + if (S_MINECRAFT_SYNC.skinBuffer.size() + size > K_MINECRAFT_SYNC_SKIN_MAX) { + S_MINECRAFT_SYNC.skinOverflow = true; + S_MINECRAFT_SYNC.skinBuffer.clear(); + return; + } + S_MINECRAFT_SYNC.skinBuffer.insert(S_MINECRAFT_SYNC.skinBuffer.end(), data, data + size); + return; + } + + if (S_MINECRAFT_SYNC.skinOverflow) { + S_MINECRAFT_SYNC.skinOverflow = false; + return; + } + + if (S_MINECRAFT_SYNC.skinBuffer.size() < 4) { + S_MINECRAFT_SYNC.skinBuffer.clear(); + S_MINECRAFT_SYNC.skinReady = false; + return; + } + + const uint16_t width = readU16Le(S_MINECRAFT_SYNC.skinBuffer.data()); + const uint16_t height = readU16Le(S_MINECRAFT_SYNC.skinBuffer.data() + 2); + const size_t pixelCount = static_cast(width) * static_cast(height); + const size_t expectedBytes = pixelCount * 2; + + if (const size_t payloadBytes = S_MINECRAFT_SYNC.skinBuffer.size() - 4; + pixelCount == 0 || payloadBytes < expectedBytes) { + S_MINECRAFT_SYNC.skinBuffer.clear(); + S_MINECRAFT_SYNC.skinReady = false; + return; + } + + S_MINECRAFT_SYNC.skinPixels.assign(pixelCount, 0); + const uint8_t* pixelData = S_MINECRAFT_SYNC.skinBuffer.data() + 4; + for (size_t i = 0; i < pixelCount; ++i) { + S_MINECRAFT_SYNC.skinPixels[i] = readU16Le(pixelData + i * 2); + } + S_MINECRAFT_SYNC.skinWidth = width; + S_MINECRAFT_SYNC.skinHeight = height; + S_MINECRAFT_SYNC.skinReady = true; + S_MINECRAFT_SYNC.skinBuffer.clear(); + } + + void minecraftSyncDraw() { + static constexpr auto skinY = 20; + static constexpr auto textY = LCD_V_RES - 20; + if (!S_MINECRAFT_SYNC.hasState && !S_MINECRAFT_SYNC.skinReady) { + return; + } + + if (S_MINECRAFT_SYNC.skinReady && !S_MINECRAFT_SYNC.skinPixels.empty()) { + const int16_t skinX = static_cast((LCD_H_RES - S_MINECRAFT_SYNC.skinWidth) / 2); + displayDriverExtensionRGBBitmapDraw( + skinX, + skinY, + static_cast(S_MINECRAFT_SYNC.skinWidth), + static_cast(S_MINECRAFT_SYNC.skinHeight), + S_MINECRAFT_SYNC.skinPixels.data() + ); + } + + if (S_MINECRAFT_SYNC.hasState) { + char modeLine[32] = {}; + char healthLine[32] = {}; + + std::snprintf(modeLine, sizeof(modeLine), "%s", S_MINECRAFT_SYNC.mode.c_str()); + std::snprintf( + healthLine, + sizeof(healthLine), + "%.1f/%.1f", + static_cast(S_MINECRAFT_SYNC.health), + static_cast(S_MINECRAFT_SYNC.maxHealth) + ); + + const uint16_t modeWidth = vision_ui_driver_str_width_get(modeLine); + const uint16_t healthWidth = vision_ui_driver_str_width_get(healthLine); + const uint16_t lineHeight = vision_ui_driver_str_height_get(); + + const int16_t modeX = static_cast((LCD_H_RES - modeWidth) / 2); + const int16_t healthX = static_cast((LCD_H_RES - healthWidth) / 2); + static constexpr int16_t healthY = textY; + const int16_t modeY = static_cast(healthY - lineHeight - 2); + + vision_ui_driver_str_draw(static_cast(modeX), static_cast(modeY), modeLine); + vision_ui_driver_str_draw(static_cast(healthX), healthY, healthLine); + } + } +} // namespace + +LumenMinecraftSync lumenGetMinecraftSync() { + return { + .initFunction = + []() { + if (!S_MINECRAFT_SYNC.serialAttached) { + serialPackAttachHandler("sync", minecraftSyncJsonHandler); + serialPackAttachHandler("sync/skin", minecraftSyncSkinHandler); + S_MINECRAFT_SYNC.skinBuffer.reserve(1024 * 10); + S_MINECRAFT_SYNC.serialAttached = true; + } + const auto config = lumenGetSystemConfig(); + vision_ui_driver_font_set(config.normal); + serialPackStart(); + }, + .loopFunction = []() { minecraftSyncDraw(); }, + .exitFunction = []() { serialPackStop(); }, + }; +} diff --git a/main/src/ui_hardware_driver.cpp b/main/src/ui_hardware_driver.cpp index a5ff369..8a5367c 100644 --- a/main/src/ui_hardware_driver.cpp +++ b/main/src/ui_hardware_driver.cpp @@ -44,9 +44,6 @@ along with this program. If not, see . // DMA block lines (must divide V_RES) #define PARALLEL_LINES 128 -#define LCD_H_RES 240 -#define LCD_V_RES 240 - u8g2_t U8G2; uint8_t G_U8G2_BUF[LCD_H_RES * LCD_V_RES / 8]; @@ -102,6 +99,28 @@ static bool onColorTransDone(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_d return false; } +static constexpr uint16_t rgb565(const uint8_t r, const uint8_t g, const uint8_t b) { + return static_cast(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)); +} + +static constexpr uint16_t U8G2_COLOR_OFF = rgb565(0, 0, 0); +static constexpr uint16_t U8G2_COLOR_ON = rgb565(255, 255, 255); + +static void displayPrepareRGBBuffers() { + static constexpr int bufSize = sizeof(S_LINES) / sizeof(S_LINES[0]); + constexpr int neededBuffers = (LCD_V_RES + PARALLEL_LINES - 1) / PARALLEL_LINES; + constexpr int buffersToClear = std::min(bufSize, neededBuffers); + for (int i = 0; i < buffersToClear; ++i) { + if (!S_LINES[i]) { + continue; + } + while (S_BUF_BUSY[i]) { + taskYIELD(); + } + std::fill_n(S_LINES[i], LCD_H_RES * PARALLEL_LINES, U8G2_COLOR_OFF); + } +} + static bool DISPLAY_READY = false; void displayFrameRender() { @@ -111,6 +130,7 @@ void displayFrameRender() { const uint32_t start = esp_timer_get_time(); vision_ui_driver_buffer_clear(); + displayPrepareRGBBuffers(); vision_ui_step_render(); const uint32_t flash = esp_timer_get_time(); @@ -243,13 +263,6 @@ void displayInit(vision_ui_action_t (*callback)()) { DISPLAY_READY = true; } -static constexpr uint16_t rgb565(const uint8_t r, const uint8_t g, const uint8_t b) { - return static_cast(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)); -} - -static constexpr uint16_t U8G2_COLOR_OFF = rgb565(0, 0, 0); -static constexpr uint16_t U8G2_COLOR_ON = rgb565(255, 255, 255); - static uint16_t MONO_TO_RGB565[256][8]; static bool LUT_READY = false; @@ -292,8 +305,10 @@ void vision_ui_driver_buffer_send() { uint16_t* row = block + line * LCD_H_RES; for (int dstX = 0; dstX < LCD_H_RES; ++dstX) { - const int srcX = (LCD_H_RES - 1) - dstX; - row[dstX] = (rowPtr[srcX] & bitMask) ? U8G2_COLOR_ON : U8G2_COLOR_OFF; + // row[dstX] = (rowPtr[srcX] & bitMask) ? U8G2_COLOR_ON : U8G2_COLOR_OFF; + if (const int srcX = (LCD_H_RES - 1) - dstX; rowPtr[srcX] & bitMask) { + row[dstX] = U8G2_COLOR_ON; + } } } @@ -303,6 +318,54 @@ void vision_ui_driver_buffer_send() { } } +void displayDriverExtensionRGBBitmapDraw( + const int16_t x, + const int16_t y, + const int16_t width, + const int16_t height, + const uint16_t* colorData +) { + if (!colorData || width <= 0 || height <= 0) { + return; + } + + const int16_t x0 = std::max(0, x); + const int16_t y0 = std::max(0, y); + const int16_t x1 = std::min(LCD_H_RES, x + width); + const int16_t y1 = std::min(LCD_V_RES, y + height); + if (x0 >= x1 || y0 >= y1) { + return; + } + + static constexpr int bufSize = sizeof(S_LINES) / sizeof(S_LINES[0]); + bool waited[bufSize] = {false}; + + for (int srcY = y0; srcY < y1; ++srcY) { + const int inY = srcY - y; + const int dstY = (LCD_V_RES - 1) - srcY; + const int bufIdx = dstY / PARALLEL_LINES; + if (bufIdx < 0 || bufIdx >= bufSize || !S_LINES[bufIdx]) { + continue; + } + if (!waited[bufIdx]) { + while (S_BUF_BUSY[bufIdx]) { + taskYIELD(); + } + waited[bufIdx] = true; + } + + const int bufYStart = bufIdx * PARALLEL_LINES; + const int rowOffset = (dstY - bufYStart) * LCD_H_RES; + + for (int srcX = x0; srcX < x1; ++srcX) { + const int inX = srcX - x; + const int dstX = (LCD_H_RES - 1) - srcX; + const uint16_t pixel = colorData[inY * width + inX]; + S_LINES[bufIdx][rowOffset + dstX] = pixel; + } + } +} + void* vision_ui_driver_buffer_pointer_get() { return G_U8G2_BUF; } diff --git a/script/display_skin_payload.py b/script/display_skin_payload.py new file mode 100644 index 0000000..83aa97c --- /dev/null +++ b/script/display_skin_payload.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +import argparse +import sys +from pathlib import Path + +try: + from PIL import Image +except ImportError as exc: + raise SystemExit("Pillow is required: pip install pillow") from exc + + +def rgb565_to_rgb888_be(data: bytes, width: int, height: int) -> Image.Image: + expected = width * height * 2 + if len(data) < expected: + raise ValueError(f"payload too small: {len(data)} < {expected}") + if len(data) > expected: + data = data[:expected] + + rgb = bytearray(width * height * 3) + dst = 0 + for i in range(0, expected, 2): + value = (data[i] << 8) | data[i + 1] # big-endian RGB565 + r = (value >> 11) & 0x1F + g = (value >> 5) & 0x3F + b = value & 0x1F + rgb[dst] = (r << 3) | (r >> 2) + rgb[dst + 1] = (g << 2) | (g >> 4) + rgb[dst + 2] = (b << 3) | (b >> 2) + dst += 3 + + return Image.frombytes("RGB", (width, height), bytes(rgb)) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Display an RGB565 big-endian payload.") + parser.add_argument("file", help="Binary payload file") + parser.add_argument("--width", type=int, required=True, help="Image width") + parser.add_argument("--height", type=int, required=True, help="Image height") + parser.add_argument("--out", help="Output PNG path (optional)") + args = parser.parse_args() + + path = Path(args.file) + if not path.exists(): + print(f"file not found: {path}", file=sys.stderr) + return 2 + + blob = path.read_bytes() + data = blob[4:] # skip width/height header + image = rgb565_to_rgb888_be(data, args.width, args.height) + + if args.out: + image.save(args.out) + else: + image.show() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/script/serial_pack_send.py b/script/serial_pack_send.py new file mode 100644 index 0000000..6ee27d5 --- /dev/null +++ b/script/serial_pack_send.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +import argparse +import struct +import sys +from pathlib import Path + +import serial + + +def main() -> int: + parser = argparse.ArgumentParser(description="Send a serial pack over USB Serial/JTAG.") + parser.add_argument("--port", default="/dev/cu.usbmodem1101", help="Serial device path") + parser.add_argument("--path", default="sync", help="Pack path (no spaces)") + parser.add_argument("--data", help="Payload string") + parser.add_argument("--file", help="Binary payload file") + parser.add_argument("--baud", type=int, default=460800, help="Baud rate") + args = parser.parse_args() + + path_bytes = args.path.encode("ascii") + if b" " in path_bytes or b"\n" in path_bytes: + print("path must not contain spaces or newlines", file=sys.stderr) + return 2 + + if args.data is None and args.file is None: + print("must provide --data or --file", file=sys.stderr) + return 2 + if args.data is not None and args.file is not None: + print("use only one of --data or --file", file=sys.stderr) + return 2 + + if args.file is not None: + file_path = Path(args.file) + if not file_path.exists(): + print(f"file not found: {file_path}", file=sys.stderr) + return 2 + data_bytes = file_path.read_bytes() + else: + data_bytes = args.data.encode("utf-8") + pkt = path_bytes + b"\n" + struct.pack("