/* Copyright 2023 Dual Tachyon * https://github.com/DualTachyon * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include "driver/py25q16.h" #include "driver/st7565.h" #include "external/printf/printf.h" #include "font.h" #include "helper/battery.h" #include "settings.h" #include "misc.h" #include "ui/helper.h" #include "ui/welcome.h" #include "ui/status.h" #include "version.h" #include "bitmaps.h" #ifdef ENABLE_FEAT_F4HWN_K5VIEWER #include "k5viewer.h" #endif #ifdef ENABLE_FEAT_F4HWN_LOGO // Boot logo storage in PY25Q16 external flash, aligned on a 4 KB sector, // placed past the calibration zone (0x010000-0x010200). // // Layout inside the sector starting at LOGO_FLASH_ADDR: // [0x00..0x07] : 8-byte header (reserved for future magic/version/flags) // [0x08..0x407]: 128x64 monochrome bitmap (1024 B) // ST7565-native: 8 pages * 128 columns, column-major LSB-top #define LOGO_FLASH_ADDR 0x011000 #define LOGO_HEADER_SIZE 8 #define LOGO_BITMAP_ADDR (LOGO_FLASH_ADDR + LOGO_HEADER_SIZE) static void UI_LoadLogo(void) { // Skip 8-byte header, then read 128x64 bitmap (1024 B): // page 0 -> gStatusLine, pages 1..7 -> gFrameBuffer. PY25Q16_ReadBuffer(LOGO_BITMAP_ADDR, gStatusLine, sizeof(gStatusLine)); PY25Q16_ReadBuffer(LOGO_BITMAP_ADDR + sizeof(gStatusLine), gFrameBuffer, sizeof(gFrameBuffer)); } void UI_DisplayLogo(void) { UI_LoadLogo(); ST7565_BlitStatusLine(); ST7565_BlitFullScreen(); } #endif #ifdef ENABLE_FEAT_F4HWN_QRCODE // QR code (version 4, 33x33 modules, EC level L) encoding: // https://github.com/armel/uv-k1-k5v3-firmware-custom // Stored in framebuffer column-major format: 5 fb-lines x 33 columns. // Each byte packs 8 vertical pixels (bit 0 = top). Last fb-line uses // only bit 0 (row 32); bits 1..7 are always 0. // // static const uint8_t BITMAP_QR_GitHub[5][33] = { // { 0x7F, 0x41, 0x5D, 0x5D, 0x5D, 0x41, 0x7F, 0x00, 0x6A, 0xB8, 0xCB, 0xA0, 0x6D, 0x07, 0xCB, 0x1F, 0xD2, 0x18, 0x59, 0x15, 0x79, 0x86, 0xCE, 0x15, 0x43, 0x00, 0x7F, 0x41, 0x5D, 0x5D, 0x5D, 0x41, 0x7F }, // { 0x87, 0xA3, 0x69, 0xC3, 0x19, 0x0E, 0x55, 0x1F, 0x43, 0x11, 0x16, 0xC1, 0x5A, 0x0E, 0x96, 0x3E, 0xA5, 0x15, 0x06, 0x2A, 0xFE, 0xCE, 0xCA, 0x3A, 0x70, 0xD9, 0xEA, 0xF5, 0x5C, 0x15, 0x8A, 0x67, 0x22 }, // { 0xE0, 0x0B, 0x1D, 0x28, 0xF5, 0x87, 0x55, 0xEB, 0xA8, 0x11, 0xA3, 0xC1, 0x5A, 0x0E, 0x96, 0x3E, 0xA5, 0x15, 0x84, 0x2B, 0x72, 0xE8, 0xE9, 0x23, 0x11, 0xCD, 0xE6, 0xC1, 0x91, 0xE6, 0x88, 0x77, 0x22 }, // { 0xFD, 0x04, 0x75, 0x75, 0x75, 0x04, 0xFD, 0x01, 0xF7, 0xE0, 0xD6, 0xC1, 0x5A, 0x0E, 0x96, 0x3E, 0xA5, 0x37, 0x22, 0x2B, 0xEA, 0xAA, 0xA7, 0x8D, 0x5F, 0x31, 0x55, 0xB1, 0x3F, 0xCE, 0xCA, 0x2C, 0x2B }, // { 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00 } // }; static const uint8_t BITMAP_QR_GitHub_Compressed[137] = { 0x7F, 0x41, 0x5D, 0x5D, 0x5D, 0x41, 0x7F, 0x00, 0x6A, 0xB8, 0xCB, 0xA0, 0x6D, 0x07, 0xCB, 0x1F, 0xD2, 0x18, 0x59, 0x15, 0x79, 0x86, 0xCE, 0x15, 0x43, 0x00, 0x7F, 0x41, 0x5D, 0x5D, 0x5D, 0x41, 0x7F, 0x87, 0xA3, 0x69, 0xC3, 0x19, 0x0E, 0x55, 0x1F, 0x43, 0x11, 0x16, 0xC1, 0x5A, 0x0E, 0x96, 0x3E, 0xA5, 0x15, 0x06, 0x2A, 0xFE, 0xCE, 0xCA, 0x3A, 0x70, 0xD9, 0xEA, 0xF5, 0x5C, 0x15, 0x8A, 0x67, 0x22, 0xE0, 0x0B, 0x1D, 0x28, 0xF5, 0x87, 0x55, 0xEB, 0xA8, 0x11, 0xA3, 0xC1, 0x5A, 0x0E, 0x96, 0x3E, 0xA5, 0x15, 0x84, 0x2B, 0x72, 0xE8, 0xE9, 0x23, 0x11, 0xCD, 0xE6, 0xC1, 0x91, 0xE6, 0x88, 0x77, 0x22, 0xFD, 0x04, 0x75, 0x75, 0x75, 0x04, 0xFD, 0x01, 0xF7, 0xE0, 0xD6, 0xC1, 0x5A, 0x0E, 0x96, 0x3E, 0xA5, 0x37, 0x22, 0x2B, 0xEA, 0xAA, 0xA7, 0x8D, 0x5F, 0x31, 0x55, 0xB1, 0x3F, 0xCE, 0xCA, 0x2C, 0x2B, 0x7F, 0x09, 0x8C, 0xA3, 0x00 }; // QR code (version 4, 33x33 modules, EC level L) encoding: // https://github.com/armel/uv-k1-k5v3-firmware-custom/wiki // Stored in framebuffer column-major format: 5 fb-lines x 33 columns. // Last fb-line uses only bit 0 (row 32); bits 1..7 are always 0. // // static const uint8_t BITMAP_QR_GitHub_Wiki[5][33] = { // { 0x7F, 0x41, 0x5D, 0x5D, 0x5D, 0x41, 0x7F, 0x00, 0x6A, 0x0F, 0x74, 0x0E, 0xD2, 0xB0, 0x74, 0xB1, 0x6D, 0x18, 0x59, 0x15, 0x79, 0x86, 0xCE, 0x15, 0x43, 0x00, 0x7F, 0x41, 0x5D, 0x5D, 0x5D, 0x41, 0x7F }, // { 0xCD, 0x5D, 0x83, 0x65, 0xE7, 0xC6, 0x55, 0xBD, 0x6B, 0x3F, 0xA9, 0x1C, 0xA5, 0xE0, 0x69, 0xE3, 0x4A, 0x15, 0x06, 0x2A, 0xFE, 0xCE, 0xCA, 0x3A, 0x70, 0xD9, 0xEA, 0xF5, 0x5C, 0x15, 0x8A, 0x67, 0x22 }, // { 0xFF, 0x25, 0xAE, 0xB6, 0x30, 0xF8, 0x55, 0xDD, 0x07, 0xB6, 0xC2, 0x1C, 0xA5, 0xE0, 0x69, 0xC4, 0x66, 0x15, 0x84, 0x2B, 0x72, 0xE8, 0xE9, 0x23, 0x11, 0xCD, 0xE6, 0xC1, 0x91, 0xE6, 0x88, 0x77, 0x22 }, // { 0xFD, 0x04, 0x74, 0x74, 0x74, 0x05, 0xFD, 0x01, 0xF7, 0xAE, 0x81, 0x1C, 0xA5, 0xE0, 0x69, 0x54, 0xFE, 0x37, 0x22, 0x2B, 0xEA, 0xAA, 0xA7, 0x8D, 0x5F, 0x31, 0x55, 0xB1, 0x3F, 0xCE, 0xCA, 0x24, 0x33 }, // { 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00 } // }; static const uint8_t BITMAP_QR_GitHub_Wiki_Compressed[137] = { 0x7F, 0x41, 0x5D, 0x5D, 0x5D, 0x41, 0x7F, 0x00, 0x6A, 0x0F, 0x74, 0x0E, 0xD2, 0xB0, 0x74, 0xB1, 0x6D, 0x18, 0x59, 0x15, 0x79, 0x86, 0xCE, 0x15, 0x43, 0x00, 0x7F, 0x41, 0x5D, 0x5D, 0x5D, 0x41, 0x7F, 0xCD, 0x5D, 0x83, 0x65, 0xE7, 0xC6, 0x55, 0xBD, 0x6B, 0x3F, 0xA9, 0x1C, 0xA5, 0xE0, 0x69, 0xE3, 0x4A, 0x15, 0x06, 0x2A, 0xFE, 0xCE, 0xCA, 0x3A, 0x70, 0xD9, 0xEA, 0xF5, 0x5C, 0x15, 0x8A, 0x67, 0x22, 0xFF, 0x25, 0xAE, 0xB6, 0x30, 0xF8, 0x55, 0xDD, 0x07, 0xB6, 0xC2, 0x1C, 0xA5, 0xE0, 0x69, 0xC4, 0x66, 0x15, 0x84, 0x2B, 0x72, 0xE8, 0xE9, 0x23, 0x11, 0xCD, 0xE6, 0xC1, 0x91, 0xE6, 0x88, 0x77, 0x22, 0xFD, 0x04, 0x74, 0x74, 0x74, 0x05, 0xFD, 0x01, 0xF7, 0xAE, 0x81, 0x1C, 0xA5, 0xE0, 0x69, 0x54, 0xFE, 0x37, 0x22, 0x2B, 0xEA, 0xAA, 0xA7, 0x8D, 0x5F, 0x31, 0x55, 0xB1, 0x3F, 0xCE, 0xCA, 0x24, 0x33, 0x7F, 0x53, 0x8E, 0xA3, 0x00 }; #endif #ifdef ENABLE_FEAT_F4HWN_MEM // Linker symbols (provided by the linker script) extern uint8_t _sdata; // Start of .data in RAM extern uint8_t _edata; // End of .data in RAM extern uint8_t _sbss; // Start of .bss in RAM extern uint8_t _ebss; // End of .bss in RAM // _eflash_used is defined by the linker at the end of the final section with a // FLASH load image. This is currently .mb_ramfunc (empty without the overlay), // after the load images for .data and .noncacheable. It therefore gives the // exact byte count that the linker reports as FLASH used. extern uint8_t _eflash_used; // Absolute symbols: their *address* IS the numeric size value (ARM/CMSIS convention). // RAM = .data + gap + .bss + heap_reserve + stack_reserve extern uint8_t _Min_Heap_Size; extern uint8_t _Min_Stack_Size; // Region sizes (must match your linker MEMORY regions) #define RAM_SIZE_BYTES (16u * 1024u) #define FLASH_SIZE_BYTES (118u * 1024u) // Base address of FLASH — must match ORIGIN(FLASH) in your linker script #define FLASH_BASE (0x08002800u) static inline uint32_t span(const void* a, const void* b) { return (uint32_t)((uintptr_t)b - (uintptr_t)a); } static void build_usage(uint32_t* ram_used, uint32_t* flash_used) { // RAM: span from start of .data to end of .bss covers .data + alignment gap + .bss. // Then add heap and stack reservations (absolute linker symbols: address = size). // Proof: (0x20002A60 - 0x20000000) + 0x200 + 0x400 = 10848 + 512 + 1024 = 12384 B ✓ const uint32_t heap_size = (uint32_t)(uintptr_t)&_Min_Heap_Size; const uint32_t stack_size = (uint32_t)(uintptr_t)&_Min_Stack_Size; *ram_used = span(&_sdata, &_ebss) + heap_size + stack_size; // FLASH: _eflash_used follows the final FLASH load image (.mb_ramfunc, // after the .data and .noncacheable load images). // Note: _etext is NOT usable here because this linker script places .rodata // sections AFTER _etext, making it an unreliable end-of-flash marker. *flash_used = span((void*)FLASH_BASE, &_eflash_used); } static inline uint16_t pct_x100(uint32_t used, uint32_t total) { return (uint16_t)((used * 10000u) / total); // 7559 => 75.59% } void UI_GetMemPercents(uint16_t *flash_pct_x100, uint16_t *ram_pct_x100) { uint32_t ram_used = 0; uint32_t flash_used = 0; build_usage(&ram_used, &flash_used); if (flash_pct_x100) *flash_pct_x100 = pct_x100(flash_used, FLASH_SIZE_BYTES); if (ram_pct_x100) *ram_pct_x100 = pct_x100(ram_used, RAM_SIZE_BYTES); } #endif #ifdef ENABLE_FEAT_F4HWN_QRCODE // Set a single pixel at LCD-physical (x, y). y=0..7 maps to gStatusLine, // y=8..63 maps to gFrameBuffer (line = (y-8)/8, bit = (y-8)%8). static void QR_SetPixel(uint8_t x, uint8_t y) { if (x >= 128 || y >= 64) return; if (y < 8) { gStatusLine[x] |= (uint8_t)(1u << y); } else { const uint8_t fb_y = (uint8_t)(y - 8u); gFrameBuffer[fb_y >> 3][x] |= (uint8_t)(1u << (fb_y & 7u)); } } // Render a square QR bitmap stored in framebuffer column-major format // (size cols × ceil(size/8) fb-lines, row-major in memory). static void QR_Draw(const uint8_t *bitmap, uint8_t size, uint8_t origin_x, uint8_t origin_y) { for (uint8_t qy = 0; qy < size; qy++) { for (uint8_t qx = 0; qx < size; qx++) { // const uint16_t idx = (uint16_t)(qy >> 3) * (uint16_t)size + (uint16_t)qx; // if ((bitmap[idx] >> (qy & 7u)) & 1u) { if (qy < 32 ? ((bitmap[(uint16_t)(qy >> 3) * (uint16_t)size + (uint16_t)qx] >> (qy & 7u)) & 1u) : ((bitmap[132 + (qx >> 3)] >> (qx & 7u)) & 1u)) { QR_SetPixel((uint8_t)(origin_x + qx), (uint8_t)(origin_y + qy)); } } } } void UI_DrawQRCode(bool wiki, uint8_t origin_x, uint8_t origin_y) { // QR_Draw(wiki ? (const uint8_t *)BITMAP_QR_GitHub_Wiki // : (const uint8_t *)BITMAP_QR_GitHub, QR_Draw(wiki ? (const uint8_t *)BITMAP_QR_GitHub_Wiki_Compressed : (const uint8_t *)BITMAP_QR_GitHub_Compressed, 33, origin_x, origin_y); } #endif void UI_DisplayReleaseKeys(void) { UI_StatusClear(); #if defined(ENABLE_FEAT_F4HWN_CTR) || defined(ENABLE_FEAT_F4HWN_INV) ST7565_ContrastAndInv(); #endif UI_DisplayClear(); UI_PrintString("RELEASE", 0, 127, 1, 10); UI_PrintString("ALL KEYS", 0, 127, 3, 10); ST7565_BlitStatusLine(); // blank status line ST7565_BlitFullScreen(); } void UI_DisplayWelcome(void) { UI_StatusClear(); #if defined(ENABLE_FEAT_F4HWN_CTR) || defined(ENABLE_FEAT_F4HWN_INV) ST7565_ContrastAndInv(); #endif UI_DisplayClear(); #ifdef ENABLE_FEAT_F4HWN ST7565_BlitStatusLine(); ST7565_BlitFullScreen(); if (gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_NONE || gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_SOUND) { ST7565_FillScreen(0x00); return; } #else if (gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_NONE || gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_FULL_SCREEN) { ST7565_FillScreen(0xFF); return; } #endif #ifdef ENABLE_FEAT_F4HWN_LOGO else if (gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_LOGO) { UI_LoadLogo(); } #endif else { char WelcomeString0[17]; char WelcomeString1[17]; char WelcomeString2[16]; char WelcomeString3[32]; // 0x0EB0 PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, WelcomeString0, 16); WelcomeString0[16] = '\0'; // 0x0EC0 PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE2_ADDR, WelcomeString1, 16); WelcomeString1[16] = '\0'; sprintf(WelcomeString2, "%u.%02uV %u%%", gBatteryVoltageAverage / 100, gBatteryVoltageAverage % 100, BATTERY_VoltsToPercent(gBatteryVoltageAverage)); if (gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_VOLTAGE) { strcpy(WelcomeString0, "VOLTAGE"); strcpy(WelcomeString1, WelcomeString2); } else if(gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_ALL) { if(strlen(WelcomeString0) == 0 && strlen(WelcomeString1) == 0) { strcpy(WelcomeString0, "WELCOME"); strcpy(WelcomeString1, WelcomeString2); } else if(strlen(WelcomeString0) == 0 || strlen(WelcomeString1) == 0) { if(strlen(WelcomeString0) == 0) { strcpy(WelcomeString0, WelcomeString1); } strcpy(WelcomeString1, WelcomeString2); } } else if(gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_MESSAGE) { if(strlen(WelcomeString0) == 0) { strcpy(WelcomeString0, "WELCOME"); } if(strlen(WelcomeString1) == 0) { strcpy(WelcomeString1, "BIENVENUE"); } } UI_PrintString(WelcomeString0, 0, 127, 0, 10); UI_PrintString(WelcomeString1, 0, 127, 2, 10); #ifdef ENABLE_FEAT_F4HWN const size_t version_width = strlen(DisplayVersion) * (ARRAY_SIZE(gFontSmall[0]) + 1u); const uint8_t version_x = version_width < LCD_WIDTH ? (uint8_t)((LCD_WIDTH - version_width + 1u) / 2u) : 0u; const uint8_t capsule_left = version_x > 2u ? (uint8_t)(version_x - 3u) : 0u; const size_t capsule_right_candidate = version_x + version_width + 2u; const uint8_t capsule_right = capsule_right_candidate < LCD_WIDTH ? (uint8_t)capsule_right_candidate : (LCD_WIDTH - 1u); UI_PrintStringSmallNormal(DisplayVersion, version_x, 0, 4); if (capsule_left > 0u) { UI_DrawLineBuffer(gFrameBuffer, 0, 35, capsule_left - 1u, 35, 1); } gFrameBuffer[4][capsule_left] ^= 0x7F; for (uint8_t x = capsule_left + 1u; x < capsule_right; x++) { gFrameBuffer[4][x] ^= 0xFF; gFrameBuffer[3][x] ^= 0x80; } gFrameBuffer[4][capsule_right] ^= 0x7F; if (capsule_right < LCD_WIDTH - 1u) { UI_DrawLineBuffer(gFrameBuffer, capsule_right + 1u, 35, LCD_WIDTH - 1u, 35, 1); } /* #ifdef ENABLE_FEAT_F4HWN_MEM uint32_t ram_used = 0; uint32_t flash_used = 0; build_usage(&ram_used, &flash_used); const uint16_t ram_pct = pct_x100(ram_used, RAM_SIZE_BYTES); const uint16_t flash_pct = pct_x100(flash_used, FLASH_SIZE_BYTES); // No floats: 7559 => 75.59% sprintf(WelcomeString3, "FLASH %u.%02u %% - SRAM %u.%02u %%", (unsigned)(flash_pct / 100), (unsigned)(flash_pct % 100), (unsigned)(ram_pct / 100), (unsigned)(ram_pct % 100)); GUI_DisplaySmallest(WelcomeString3, 5, 1, true, true); ST7565_BlitStatusLine(); #endif */ sprintf(WelcomeString3, "%s Edition", Edition); UI_PrintStringSmallNormal(WelcomeString3, 0, 127, 6); #else UI_PrintStringSmallNormal(Version, 0, 127, 6); #endif } ST7565_BlitStatusLine(); ST7565_BlitFullScreen(); #ifdef ENABLE_FEAT_F4HWN_K5VIEWER K5VIEWER_Update(true); #endif }