#include "McpScreen.h" #include #include #include #include #include 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; }