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