Files
tactility_apps/Apps/McpScreen/main/Source/DisplayTools.c
T
Adolfo Reyna 0fb3c22f79
Main / Build (Brainfuck) (push) Has been cancelled
Main / Build (Breakout) (push) Has been cancelled
Main / Build (Calculator) (push) Has been cancelled
Main / Build (Diceware) (push) Has been cancelled
Main / Build (EpubReader) (push) Has been cancelled
Main / Build (GPIO) (push) Has been cancelled
Main / Build (GraphicsDemo) (push) Has been cancelled
Main / Build (HelloWorld) (push) Has been cancelled
Main / Build (M5UnitTest) (push) Has been cancelled
Main / Build (Magic8Ball) (push) Has been cancelled
Main / Build (MediaKeys) (push) Has been cancelled
Main / Build (MystifyDemo) (push) Has been cancelled
Main / Build (SerialConsole) (push) Has been cancelled
Main / Build (Snake) (push) Has been cancelled
Main / Build (TamaTac) (push) Has been cancelled
Main / Build (TodoList) (push) Has been cancelled
Main / Build (TwoEleven) (push) Has been cancelled
Main / Bundle (push) Has been cancelled
feat: add McpScreen phase 2 display tools
2026-06-25 12:43:08 -04:00

338 lines
10 KiB
C

#include "McpScreen.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <esp_log.h>
#include <tt_lvgl.h>
static const char* TAG = "DisplayTools";
static bool ascii_space(uint8_t value) {
return value == ' ' || value == '\t' || value == '\r' ||
value == '\n' || value == '\f' || value == '\v';
}
static bool ascii_digit(uint8_t value) {
return value >= '0' && value <= '9';
}
static uint16_t read_le16(const uint8_t* value) {
return (uint16_t)value[0] | ((uint16_t)value[1] << 8);
}
static uint32_t read_le32(const uint8_t* value) {
return (uint32_t)value[0] |
((uint32_t)value[1] << 8) |
((uint32_t)value[2] << 16) |
((uint32_t)value[3] << 24);
}
static bool display_ready(McpScreenState* state) {
return state != NULL &&
state->visible &&
state->draw_area != NULL &&
state->framebuffer != NULL &&
state->draw_width > 0 &&
state->draw_height > 0;
}
static uint16_t rgb888_to_rgb565(uint8_t red, uint8_t green, uint8_t blue) {
return (uint16_t)(((red & 0xF8) << 8) |
((green & 0xFC) << 3) |
(blue >> 3));
}
static void put_pixel(McpScreenState* state, int x, int y, uint16_t color) {
if (x >= 0 && y >= 0 && x < state->draw_width && y < state->draw_height) {
state->framebuffer[(size_t)y * state->draw_width + x] = color;
}
}
bool mcp_ui_draw_rgb565_be(
McpScreenState* state,
const uint8_t* data,
size_t data_size,
int x,
int y,
int width,
int height
) {
if (data == NULL || width <= 0 || height <= 0 ||
data_size < (size_t)width * height * 2 || !display_ready(state)) {
return false;
}
bool success = false;
if (tt_lvgl_lock(pdMS_TO_TICKS(2000))) {
if (display_ready(state)) {
for (int source_y = 0; source_y < height; ++source_y) {
int destination_y = y + source_y;
if (destination_y < 0 || destination_y >= state->draw_height) {
continue;
}
for (int source_x = 0; source_x < width; ++source_x) {
int destination_x = x + source_x;
if (destination_x < 0 || destination_x >= state->draw_width) {
continue;
}
size_t offset = ((size_t)source_y * width + source_x) * 2;
uint16_t color = ((uint16_t)data[offset] << 8) | data[offset + 1];
put_pixel(state, destination_x, destination_y, color);
}
}
lv_obj_invalidate(state->draw_area);
state->override_active = true;
success = true;
}
tt_lvgl_unlock();
}
return success;
}
bool mcp_ui_draw_bmp(
McpScreenState* state,
const uint8_t* data,
size_t data_size,
int x,
int y
) {
if (data == NULL || data_size < 54 || !display_ready(state) ||
data[0] != 'B' || data[1] != 'M') {
return false;
}
uint32_t pixel_offset = read_le32(data + 10);
uint32_t dib_size = read_le32(data + 14);
int32_t width = (int32_t)read_le32(data + 18);
int32_t signed_height = (int32_t)read_le32(data + 22);
uint16_t planes = read_le16(data + 26);
uint16_t bits_per_pixel = read_le16(data + 28);
uint32_t compression = read_le32(data + 30);
if (dib_size < 40 || width <= 0 || signed_height == 0 || planes != 1 ||
(bits_per_pixel != 24 && bits_per_pixel != 32) || compression != 0) {
return false;
}
int height = signed_height < 0 ? -signed_height : signed_height;
bool top_down = signed_height < 0;
size_t row_stride = (((size_t)width * bits_per_pixel + 31) / 32) * 4;
if (pixel_offset > data_size ||
row_stride > data_size ||
(size_t)height > (data_size - pixel_offset) / row_stride) {
return false;
}
bool success = false;
if (tt_lvgl_lock(pdMS_TO_TICKS(2500))) {
if (display_ready(state)) {
size_t bytes_per_pixel = bits_per_pixel / 8;
for (int source_y = 0; source_y < height; ++source_y) {
int file_y = top_down ? source_y : (height - 1 - source_y);
const uint8_t* row = data + pixel_offset + (size_t)file_y * row_stride;
for (int source_x = 0; source_x < width; ++source_x) {
const uint8_t* pixel = row + (size_t)source_x * bytes_per_pixel;
put_pixel(
state,
x + source_x,
y + source_y,
rgb888_to_rgb565(pixel[2], pixel[1], pixel[0])
);
}
}
lv_obj_invalidate(state->draw_area);
state->override_active = true;
success = true;
}
tt_lvgl_unlock();
}
return success;
}
static bool pbm_next_number(
const uint8_t* data,
size_t data_size,
size_t* offset,
int* result
) {
while (*offset < data_size) {
if (data[*offset] == '#') {
while (*offset < data_size && data[*offset] != '\n') {
(*offset)++;
}
} else if (ascii_space(data[*offset])) {
(*offset)++;
} else {
break;
}
}
if (*offset >= data_size || !ascii_digit(data[*offset])) {
return false;
}
int value = 0;
while (*offset < data_size && ascii_digit(data[*offset])) {
value = value * 10 + (data[*offset] - '0');
(*offset)++;
}
*result = value;
return true;
}
bool mcp_ui_draw_pbm(
McpScreenState* state,
const uint8_t* data,
size_t data_size,
int x,
int y
) {
if (data == NULL || data_size < 8 || !display_ready(state) ||
data[0] != 'P' || data[1] != '4') {
ESP_LOGE(
TAG,
"PBM precondition failed data=%p size=%u ready=%d magic=%02x%02x",
data,
(unsigned)data_size,
display_ready(state),
data_size > 0 ? data[0] : 0,
data_size > 1 ? data[1] : 0
);
return false;
}
size_t offset = 2;
int width = 0;
int height = 0;
if (!pbm_next_number(data, data_size, &offset, &width) ||
!pbm_next_number(data, data_size, &offset, &height) ||
width <= 0 || height <= 0) {
ESP_LOGE(TAG, "PBM dimension parse failed offset=%u w=%d h=%d", (unsigned)offset, width, height);
return false;
}
if (offset >= data_size || !ascii_space(data[offset])) {
ESP_LOGE(
TAG,
"PBM missing header separator offset=%u size=%u byte=%02x",
(unsigned)offset,
(unsigned)data_size,
offset < data_size ? data[offset] : 0
);
return false;
}
if (data[offset] == '\r' && offset + 1 < data_size && data[offset + 1] == '\n') {
offset += 2;
} else {
offset++;
}
size_t row_bytes = ((size_t)width + 7) / 8;
if (offset > data_size ||
(size_t)height > (data_size - offset) / row_bytes) {
ESP_LOGE(
TAG,
"PBM payload too small offset=%u size=%u row=%u h=%d",
(unsigned)offset,
(unsigned)data_size,
(unsigned)row_bytes,
height
);
return false;
}
bool success = false;
if (tt_lvgl_lock(pdMS_TO_TICKS(2000))) {
if (display_ready(state)) {
for (int source_y = 0; source_y < height; ++source_y) {
const uint8_t* row = data + offset + (size_t)source_y * row_bytes;
for (int source_x = 0; source_x < width; ++source_x) {
bool black = (row[source_x >> 3] & (0x80 >> (source_x & 7))) != 0;
put_pixel(state, x + source_x, y + source_y, black ? 0x0000 : 0xFFFF);
}
}
lv_obj_invalidate(state->draw_area);
state->override_active = true;
success = true;
}
tt_lvgl_unlock();
} else {
ESP_LOGE(TAG, "PBM LVGL lock timed out");
}
if (!success) ESP_LOGE(TAG, "PBM draw failed after lock ready=%d", display_ready(state));
return success;
}
static char* base64_encode(const uint8_t* input, size_t input_size) {
static const char alphabet[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t output_size = ((input_size + 2) / 3) * 4;
char* output = malloc(output_size + 1);
if (output == NULL) {
return NULL;
}
size_t in = 0;
size_t out = 0;
while (in < input_size) {
uint32_t value = (uint32_t)input[in++] << 16;
bool have_second = in < input_size;
if (have_second) value |= (uint32_t)input[in++] << 8;
bool have_third = in < input_size;
if (have_third) value |= input[in++];
output[out++] = alphabet[(value >> 18) & 0x3F];
output[out++] = alphabet[(value >> 12) & 0x3F];
output[out++] = have_second ? alphabet[(value >> 6) & 0x3F] : '=';
output[out++] = have_third ? alphabet[value & 0x3F] : '=';
}
output[out] = '\0';
return output;
}
char* mcp_ui_get_screenshot_pbm_base64(McpScreenState* state) {
if (!display_ready(state)) {
return NULL;
}
size_t row_bytes = ((size_t)state->draw_width + 7) / 8;
char header[40];
int header_size = snprintf(
header,
sizeof(header),
"P4\n%u %u\n",
state->draw_width,
state->draw_height
);
size_t pbm_size = (size_t)header_size + row_bytes * state->draw_height;
uint8_t* pbm = calloc(1, pbm_size);
if (pbm == NULL) {
return NULL;
}
memcpy(pbm, header, header_size);
bool success = false;
if (tt_lvgl_lock(pdMS_TO_TICKS(2000))) {
if (display_ready(state)) {
for (int y = 0; y < state->draw_height; ++y) {
uint8_t* row = pbm + header_size + (size_t)y * row_bytes;
for (int x = 0; x < state->draw_width; ++x) {
uint16_t color = state->framebuffer[(size_t)y * state->draw_width + x];
int red = (color >> 11) & 0x1F;
int green = (color >> 5) & 0x3F;
int blue = color & 0x1F;
int luminance = red * 2 + green * 3 + blue;
if (luminance < 128) {
row[x >> 3] |= (0x80 >> (x & 7));
}
}
}
success = true;
}
tt_lvgl_unlock();
}
char* encoded = success ? base64_encode(pbm, pbm_size) : NULL;
free(pbm);
return encoded;
}