Exposure unit
for expose PCB by UV light
BananaPi sigle board computer
Banana Pi is an open source hardware project lead by GuangDong BiPai technology co., LTD.
Radiation monitor with BananaPi Zero.
Screenshot of Grafana data interpretation of radiation meter
Load dependent speed controller of the mini drill
The basic equipment of every electrical engineering or model maker's workshop
DPS with soldered components
view of the mounted printed circuit board exported from CAD
Reflow oven
for soldering SMD prited circuits board
Reflow oven
also suitable for drying solid materials
RC433 for HomeAsistant
remote controler for garage door from HomeAssistant

Large fonts in micropythone on ESP32

               Micropython as a means of creating embedded system programs is becoming increasingly popular and gaining popularity. Often the limiting factor is the size of the operating memory or flash memory. Therefore, various images of the micropython system often have limited functionality by omitting some functions. Very often these are image and letter fonts that take up a lot of memory. Such a case is the image of micropython used for the popular esp32 series microcomputer in combination with the LVGL library.

The described library helps solve this problem and the illustrative image shows a screen of a laboratory source using this library.

I based my research on the following criteria:
- minimum possible need for operating memory
- minimum possible consumption of program memory (flash or sd card)
- good ability to redraw the screen
- minimize flicker
- possibility of enlarging, reducing characters
- possibility of combining any fonts and symbols

I used my own library containing the following code:

 

# large_font.py - Library for large digit displays with zoom capability
import lvgl as lv
import os, gc

class DisplayManager:
    def __init__(self, width=800, height=480):
        self.WIDTH = width
        self.HEIGHT = height
        self.current_screen = None
        
    def create_screen(self, bg_color=lv.color_hex(0x000000)):
        """Create a new screen with optional background color"""
        screen = lv.obj()
        screen.set_size(self.WIDTH, self.HEIGHT)
        screen.set_style_bg_color(bg_color, lv.PART.MAIN)
        return screen
    
    def show_screen(self, screen):
        """Display the specified screen"""
        screen.set_parent(lv.scr_act())
        self.current_screen = screen
        lv.scr_load(screen)

class DigitDisplay:
    def __init__(self, display_manager, x_pos, y_pos, suffix=None, zoom=128):
        self.dm = display_manager
        self.img_pool = {}  # Image pool for all digits
        self.current_screen = None
        self.x_pos = x_pos
        self.y_pos = y_pos
        self.base_digit_spacing = 50  # Základný rozostup pri zoom=255 (100%)
        self.base_comma_offset = 25   # Základný offset desatinnej čiarky pri zoom=255
        self.base_suffix_offset = 5   # Základný offset sufixu pri zoom=255
        self.max_digits = 4  # For 4-digit display
        self.img_usage = {}
        self.suffix = suffix
        self.zoom = zoom  # Zoom factor (255 = 100%, 128 = 50%)
        # Vypočítame aktuálne rozostupy podľa zoomu
        self._update_spacing()
        
    def init_digits(self, screen):
        """Initialize display with parent screen"""
        self.current_screen = screen
        self._load_digit_images()
        
    def _load_bmp(self, filename):
        """Load BMP image from file"""
        try:
            with open(filename, 'rb') as f:
                data = f.read()

            if data[0:2] != b'BM':
                raise ValueError("Not a valid BMP file")

            width = int.from_bytes(data[18:22], 'little')
            height = int.from_bytes(data[22:26], 'little')
            bpp = int.from_bytes(data[28:30], 'little')
            data_offset = int.from_bytes(data[10:14], 'little')

            if bpp != 24:
                raise ValueError("Only 24-bit BMP supported")

            row_padding = (4 - (width * 3) % 4) % 4
            img_data = bytearray(width * height * 2)

            for y in range(height):
                for x in range(width):
                    bmp_pos = data_offset + (height - 1 - y) * (width * 3 + row_padding) + x * 3
                    b, g, r = data[bmp_pos], data[bmp_pos+1], data[bmp_pos+2]
                    rgb565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
                    img_data[(y * width + x) * 2] = rgb565 & 0xFF
                    img_data[(y * width + x) * 2 + 1] = (rgb565 >> 8) & 0xFF

            img_dsc = lv.img_dsc_t({
                'data_size': len(img_data),
                'data': img_data,
                'header': {
                    'w': width,
                    'h': height,
                    'cf': lv.img.CF.TRUE_COLOR
                }
            })

            img = lv.img(self.current_screen)
            img.set_src(img_dsc)
            img.set_pos(-1000, -1000)  # Hide initially
            return img
        except Exception as e:
            print(f"Error loading {filename}: {e}")
            return None

    def _load_digit_images(self):
        """Load all digit images into pool"""
        digit_files = {
            '0': '30.bmp',
            '1': '31.bmp',
            '2': '32.bmp',
            '3': '33.bmp',
            '4': '34.bmp',
            '5': '35.bmp',
            '6': '36.bmp',
            '7': '37.bmp',
            '8': '38.bmp',
            '9': '39.bmp',
            ',': '2e.bmp',
            'V': 'V.bmp',
            'A': 'A.bmp'
        }

        for digit, filename in digit_files.items():
            self.img_pool[digit] = []
            original_img = self._load_bmp(filename)
            if not original_img:
                print(f"Warning: Failed to load '{digit}' from {filename}")
                continue

            src = original_img.get_src()
            original_img.set_pos(-1000, -1000)
            self.img_pool[digit].append(original_img)

            # Create extra copies for each digit position
            for _ in range(self.max_digits - 1):
                img = lv.img(self.current_screen)
                img.set_src(src)
                img.set_pos(-1000, -1000)
                self.img_pool[digit].append(img)
    
    def _get_next_img(self, digit):
        """Get next available image for digit from pool"""
        if digit not in self.img_usage:
            self.img_usage[digit] = 0
        index = self.img_usage[digit]
        pool = self.img_pool.get(digit, [])
        if index < len(pool):
            img = pool[index]
            self.img_usage[digit] += 1
            return img
        else:
            print(f"Not enough images for digit '{digit}'!")
            return None

    def display_value(self, int_part, frac_part):
        """Display a numeric value with decimal point"""
        str_int = f"{int_part:02d}"  # 2-digit integer part
        str_frac = f"{frac_part:02d}"  # 2-digit fractional part
        all_digits = list(str_int + str_frac)  # Combine all digits
        
        self.img_usage = {}  # Reset image usage tracking
        
        # Pred zobrazením prepočítame rozostupy podľa aktuálneho zoomu
        self._update_spacing()
        
        # Display integer part digits (first 2 digits)
        for i in range(2):
            digit = all_digits[i]
            pos_x = self.x_pos + i * self.digit_spacing
            
            # Hide leading zero
            if i == 0 and digit == '0':
                for other_digit, pool in self.img_pool.items():
                    for other_img in pool:
                        if other_img.get_x() == pos_x:
                            other_img.set_pos(-1000, self.y_pos)
                continue
                
            img = self._get_next_img(digit)
            if img:
                # Hide any other image at this position
                for other_digit, pool in self.img_pool.items():
                    for other_img in pool:
                        if other_img is not img and other_img.get_x() == pos_x:
                            other_img.set_pos(-1000, self.y_pos)
                img.set_pos(pos_x, self.y_pos)
                img.set_zoom(self.zoom)

        # Display decimal comma
        comma_pos_x = self.x_pos + 2 * self.digit_spacing
        comma_img = self._get_next_img(',')
        if comma_img:
            comma_img.set_pos(comma_pos_x, self.y_pos)
            comma_img.set_zoom(self.zoom)

        # Display fractional part digits (last 2 digits)
        for i in range(2, 4):
            digit = all_digits[i]
            pos_x = self.x_pos + (i) * self.digit_spacing + self.comma_offset
            
            img = self._get_next_img(digit)
            if img:
                for other_digit, pool in self.img_pool.items():
                    for other_img in pool:
                        if other_img is not img and other_img.get_x() == pos_x:
                            other_img.set_pos(-1000, self.y_pos)
                img.set_pos(pos_x, self.y_pos)
                img.set_zoom(self.zoom)

        # Display suffix if specified
        if self.suffix in self.img_pool:
            suffix_img = self._get_next_img(self.suffix)
            if suffix_img:
                suffix_x = self.x_pos + 4 * self.digit_spacing + self.comma_offset + self.suffix_offset
                # Nastavenie priamej pozície pre suffix
                suffix_img.set_pos(suffix_x, self.y_pos)
                suffix_img.set_zoom(self.zoom)

    def _update_spacing(self):
        """Prepočíta rozostupy medzi číslicami podľa aktuálneho zoomu"""
        zoom_factor = self.zoom / 255.0  # 255 = 100%, 128 = 50%
        self.digit_spacing = int(self.base_digit_spacing * zoom_factor)
        self.comma_offset = int(self.base_comma_offset * zoom_factor)
        self.suffix_offset = int(self.base_suffix_offset * zoom_factor)
        
    def set_zoom(self, zoom_factor):
        """Set zoom factor for all digits (255 = 100%)"""
        self.zoom = zoom_factor
        self._update_spacing()  # Prepočítame rozostupy po zmene zoomu

def create_4_digit_displays(dm, screen, zoom=255):
    """Create 4 digit displays with specified zoom level (255 = 100%)"""
    digit_displays = []
    positions = [
        (70, 50),  # Top left (V)
        (70, 200),  # Top right (V)
        (70, 300),  # Bottom left (A)
        (70, 370),  # Bottom right (A)
    ]
    suffixes = ['V', 'V', 'A', 'A']

    for (x, y), suffix in zip(positions, suffixes):
        disp = DigitDisplay(dm, x, y, suffix=suffix)
        disp.set_zoom(zoom)
        disp.init_digits(screen)
        digit_displays.append(disp)

    return digit_displays

def refresh_digit_displays(digit_displays, num1, num2, num3, num4):
    """Refresh all 4 digit displays with new values"""
    values = [
        (num1 // 100, num1 % 100),  # First display (voltage)
        (num2 // 100, num2 % 100),  # Second display (voltage)
        (num3 // 100, num3 % 100),  # Third display (current)
        (num4 // 100, num4 % 100)   # Fourth display (current)
    ]
    
    for disp, (int_p, frac_p) in zip(digit_displays, values):
        disp.display_value(int_p, frac_p)
    
    return values

 

 Solution :

        I used a method where the displayed characters are images. Each character is a BMP image. The image must be 24 bit, otherwise the LVGL library cannot import it. I chose images of 50x96 points. whose size is around 10kB. With large displayed characters, the number of them on the screen that can be meaningfully placed is limited anyway, so in the function def _load_digit_images(self) I defined the characters and the corresponding bmp files that will be displayed somewhere on the screen. The number of loaded images can be changed arbitrarily. Each image is loaded into memory only once. But we need to display it x times in different places.  For this purpose, I used the lvgl.scr_load() function call, which creates an image object, but only using a reference to an already loaded BMP image, which significantly saves RAM. For each displayed position, I loaded images into the pool that could potentially be required for display at that position. Now, all I need to do is swap the images at that position. It's simple, but slow and the display refresh "flashes terribly". The solution is that all characters are loaded into a pile at that position and those that are not currently displayed are shifted to a position outside the display area, which is fast and efficient and can be done simply by calling the lvgl.img.set_offset_x() function. To optimize speed, only characters where a change is required are redrawn on the screen. This approach allows for fast changes (about 40 large image characters on the screen per second). Another advantage is the ability to enlarge or reduce the loops and thus achieve the display of any character size.

     The disadvantage of this approach is that we are manipulating BMP images that have a fixed foreground and background color, so changing the background transparency or the color of the characters or background by changing the LVGL attributes is not possible.

         Example of use in the main program, which serves as the main application to display four different values ​​at different zoom levels, for illustrative purposes:

import lvgl as lv
import display
import large_fonts

display.init()

# Initialize display manager
dm = large_fonts.DisplayManager()

# Create screen
screen = dm.create_screen()
dm.show_screen(screen)

# Create 4 digit displays with different zoom levels
displays = large_fonts.create_4_digit_displays(dm, screen, zoom=128)
displays[0].set_zoom(512) # 200% zoom
displays[1].set_zoom(255) # 100% zoom
displays[2].set_zoom(128) # 50% zoom
displays[3].set_zoom(64) # 25% zoom

# Update displays with values
large_fonts.refresh_digit_displays(displays, 1234, 5678, 9012, 3456)

import snapshot

Results can be see on followinf screenshot:

large_ font

Description of classes, functions, atributes:

Documentation for large_fonts.py Library

Introduction

The large_fonts.py library provides tools for displaying large digits on screens using bitmap images. It's designed for use with the LVGL library and allows smooth resizing of displayed digits using zoom functionality.

DisplayManager Class

Description

Base class for display and screen management.

Attributes

  • WIDTH (int): Display width in pixels (default 800)
  • HEIGHT (int): Display height in pixels (default 480)
  • current_screen (lv.obj): Reference to currently displayed screen

Methods

create_screen(bg_color=lv.color_hex(0x000000))

Creates a new screen with given background color.

Parameters:

  • bg_color: Background color (default black)

Returns:

  • lv.obj: Created screen object

show_screen(screen)

Displays the specified screen.

Parameters:

  • screen: Screen object to display

DigitDisplay Class

Description

Class for displaying large digits with zoom capability and decimal point.

Attributes

  • img_pool (dict): Pool of images for digits
  • current_screen (lv.obj): Parent screen
  • x_pos, y_pos (int): Display position
  • base_digit_spacing (int): Base digit spacing at 100% zoom (255)
  • base_comma_offset (int): Base decimal point offset at 100% zoom
  • base_suffix_offset (int): Base suffix offset at 100% zoom
  • max_digits (int): Maximum number of digits (default 4)
  • img_usage (dict): Image usage tracking
  • suffix (str): Display suffix (e.g. 'V', 'A')
  • zoom (int): Current zoom value (255 = 100%)
  • digit_spacing (int): Current digit spacing (calculated from zoom)
  • comma_offset (int): Current decimal point offset
  • suffix_offset (int): Current suffix offset

Methods

init_digits(screen)

Initializes the display with given parent screen.

Parameters:

  • screen: Parent screen object

display_value(int_part, frac_part)

Displays a numeric value with decimal point.

Parameters:

  • int_part: Integer part (2 digits)
  • frac_part: Fractional part (2 digits)

set_zoom(zoom_factor)

Sets zoom scale for all digits.

Parameters:

  • zoom_factor: Zoom value (255 = 100%, 128 = 50%)

_update_spacing()

Internal method for recalculating spacing based on current zoom.

_load_bmp(filename)

Loads BMP image from file.

Parameters:

  • filename: Filename to load

Returns:

  • lv.img or None on error

_load_digit_images()

Loads all required digit images into pool.

_get_next_img(digit)

Gets next available image for given digit from pool.

Parameters:

  • digit: Digit character ('0'-'9', ',')

Returns:

  • lv.img or None if not available

Functions

create_4_digit_displays(dm, screen, zoom=255)

Creates 4 digit displays (2 for voltage, 2 for current) with given zoom scale.

Parameters:

  • dm: DisplayManager instance
  • screen: Target screen
  • zoom: Initial zoom value (default 255 = 100%)

Returns:

  • list: List of 4 DigitDisplay instances

refresh_digit_displays(digit_displays, num1, num2, num3, num4)

Updates all 4 displays with new values.

Parameters:

  • digit_displays: List of displays from create_4_digit_displays
  • num1, num2, num3, num4: New values for displays (as integers, e.g. 1234 = 12.34)

Returns:

  • list: List of values in format [(int_part, frac_part), ...]

Usage Example

import large_fonts
import lvgl as lv

Initialization

dm = large_fonts.DisplayManager()
screen = dm.create_screen()
dm.show_screen(screen)

Create displays with 150% zoom

displays = large_fonts.create_4_digit_displays(dm, screen, zoom=382)

Display values

large_fonts.refresh_digit_displays(displays, 1234, 5678, 9012, 3456)

Change zoom

for disp in displays:
disp.set_zoom(200) # ~80% of original size

Notes

  • Library requires BMP files with digits in working directory
  • Zoom value 255 corresponds to 100% size (1:1)
  • For proper display, call lv.task_handler() in main loop
  • Digits are shown/hidden by moving them on/off screen (x=-1000)

...
 

 

Related Articles

Copyright © Free Joomla! 4 templates / Design by Galusso Themes