diff --git a/README.md b/README.md index 7977b13..05a7074 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ You can buy one of these great little I2C LCD on eBay or somewhere like [the Pi - [Backlight control](#backlight-control) - [Custom characters](#custom-characters) - [Extended strings](#extended-strings) + - [Emulator](#emulator) - [Forex](#forex) - [Home automation](#home-automation) - [IP address](#ip-address) @@ -119,6 +120,78 @@ Please, see the comments and implementation in the [`demo_lcd_custom_characters.

+### Emulator + +- Author: [@Jumitti](https://github.com/Jumitti) +- ``Tkinter`` Python package: standard library (on **WINDOWS**). + - If there is an error, how to install on **Debian/Ubuntu**: + ```bash + sudo apt update + sudo apt install python3-tk + ``` + On **macOS**: + ```bash + brew install python-tk + ``` + +Just a 16x2 LCD screen simulator using Tkinter, allowing for easy testing and visualization of LCD displays without physical hardware. This simulator helps in developing and debugging LCD-based projects directly from a computer. +Especially if you don't have your Raspberry and LCD with you. + +Some nice features are in addition. You can change the title and size of the Tkinter window, change the font and the +font size, the color of the background and the text (the [Backlight control](#backlight-control) is reverse colors). + +The font size and [Custom characters](#custom-characters) adapt to the size of the Tkinter window. +And because it is an emulator, if you wanted to add or remove columns and lines you can do it + + +**How to use:** +- ``import drivers`` => ``import emulators`` +- ``Lcd()`` => ``LcdEmulator()`` +- ``CustomCharacters`` => ``CustomCharactersEmulator()`` + +Suggested beginning program: + +```Python +import emulators + +display = emulators.LcdEmulator() +cc = emulators.CustomCharactersEmulator(display) + +# Then the rest of your finest feats +``` + +All other functions are the same. **Please refer to [demo_emulator.py](demo_emulator.py)** + +**Personalization of your Tkinter window** + +- **Parameters for `LcdEmulator()`** +```Python +import emulators + +display = emulators.LcdEmulator(TITTLE_WINDOWS="Hello World", + LCD_BACKGROUND="pale turquoise", LCD_FOREGROUND="orange", + SESSION_STATE_BACKLIGHT=1, + FONT="Arial", FONT_SIZE=50, + COLUMNS=16, ROWS=2, CHAR_WIDTH=50) +``` + +| Parameter | Type | Default Value | Description | +|-------------------------|-------------------|----------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| `TITTLE_WINDOWS` | `Union[str, int]` | `LCD 16x2 Emulator` | Renames the Tkinter window. | +| `LCD_BACKGROUND` | `str` | `green` | Background color (supports HEX values, e.g., `#55A8DB`). [List of supported colors](https://www.tcl-lang.org/man/tcl8.4/TkCmd/colors.htm). | +| `LCD_FOREGROUND` | `str` | `black` | Foreground color (supports HEX values, e.g., `#55A8DB`). [List of supported colors](https://www.tcl-lang.org/man/tcl8.4/TkCmd/colors.htm). | +| `SESSION_STATE_BACKLIGHT` | `int` | `1` | Controls the backlight. Set `1` for ON and `0` for OFF. | +| `FONT` | `str` | `Courier` | Font family. [List of available fonts](https://stackoverflow.com/a/64301819). | +| `FONT_SIZE` | `int > 0` | `75` | The font size. The font size adapts to the window size to prevent overflow. | +| `COLUMNS` | `int > 0` | `16` | Number of columns (you can customize it beyond the standard 16x2 LCD screen configuration). | +| `ROWS` | `int > 0` | `2` | Number of rows (you can customize it beyond the standard 16x2 LCD screen configuration). | +| `CHAR_WIDTH` | `int > 0` | `75` | Resizes the Tkinter window accordingly. | + + +

+ +

+ ### Extended strings - Author: [@juvus](https://github.com/juvus) diff --git a/demo_emulator.py b/demo_emulator.py new file mode 100644 index 0000000..44e9d76 --- /dev/null +++ b/demo_emulator.py @@ -0,0 +1,63 @@ +#! /usr/bin/env python + +# Just a 16x2 LCD screen simulator using Tkinter, allowing for easy testing and +# visualization of LCD displays without physical hardware. This simulator helps +# in developing and debugging LCD-based projects directly from a computer. + +# Import necessary libraries for communication and display use +import emulators +from time import sleep +from datetime import datetime + + +# Load the driver and set it to "display" +# If you use something from the driver library use the "display." prefix first +display = emulators.LcdEmulator() + +# Create object with custom characters data +cc = emulators.CustomCharactersEmulator(display) + +# Redefine the default characters: +# Custom caracter #1. Code {0x00}. +cc.char_1_data = ["01010", "11111", "10001", "10101", "10001", "11111", "01010", "00000"] + +# Load custom characters data to CG RAM: +cc.load_custom_characters_data() + +# Main body for code +try: + i = 0 + while True: + display.lcd_clear() + + display.lcd_display_string(' Hello, World !', line=1) + if i < 1: + display.lcd_backlight(1) + text = "This is a simulation of a 16x2 LCD" + for j in range(len(text) - 14): + text2 = "{0x00}" + text[j:j + 15] + display.lcd_display_extended_string(text2, 2) + sleep(0.15) + i += 1 + + elif 1 <= i <= 10: + display.lcd_backlight(0) + display.lcd_display_string(str(datetime.now().time()), 2) + i += 1 + + elif 11 <= i <= 20: + display.lcd_display_string(" ENJOY :) ", line=2) + i += 1 + + if i > 20: + i = 0 + + sleep(0.5) + +except KeyboardInterrupt: + # If there is a KeyboardInterrupt (when you press ctrl+c), exit the program and cleanup + print("Cleaning up!") + display.lcd_clear() + + + diff --git a/emulators/__init__.py b/emulators/__init__.py new file mode 100644 index 0000000..7580b28 --- /dev/null +++ b/emulators/__init__.py @@ -0,0 +1 @@ +from .emulator import LcdEmulator, CustomCharactersEmulator \ No newline at end of file diff --git a/emulators/emulator.py b/emulators/emulator.py new file mode 100644 index 0000000..3c18822 --- /dev/null +++ b/emulators/emulator.py @@ -0,0 +1,344 @@ +import tkinter as tk +from tkinter import font +from typing import List, Optional, Union +from re import match +from time import sleep + +custom_chars = {} # Storage CustomCharacters + + +class LcdEmulator: + def __init__(self, + TITTLE_WINDOWS: Union[str, int] = "LCD 16x2 Emulator", + LCD_BACKGROUND: str = "green", + LCD_FOREGROUND: str = "black", + SESSION_STATE_BACKLIGHT: int = 1, + FONT: str = "Courier", + FONT_SIZE: int = 75, + COLUMNS: int = 16, + ROWS: int = 2, + CHAR_WIDTH: int = 75): + + """ + Parameters + ---------- + TITTLE_WINDOWS: Union[str, int], default "LCD 16x2 Emulator" + As expected, rename the TKinter windows + LCD_BACKGROUND: str, default "green", HEX allowed (e.g #55A8DB), + List: https://www.tcl-lang.org/man/tcl8.4/TkCmd/colors.htm + Control background color. + LCD_FOREGROUND: str, default "black", HEX allowed (e.g #55A8DB), + List: https://www.tcl-lang.org/man/tcl8.4/TkCmd/colors.htm + Control foreground color (generally the text unless you do SESSION_STATE_BACKLIGHT = 0 (OFF)). + SESSION_STATE_BACKLIGHT: int, default 1, 1: BACKLIGHT ON | 0: BACKLIGHT OFF + Control the backlight, 1 for ON and 0 for OFF. + FONT: str, default "Courier", List: https://stackoverflow.com/a/64301819 + Change font. + FONT_SIZE: int, default 75, + The font size also adapt to the size of the windows so as not to exceed. + COLUMNS: int, default 16, + Unlike the 16x2 LCD screen, here we can have several columns + ROWS: int, default 2, + Unlike the 16x2 LCD screen, here we can have several rows + CHAR_WIDTH: int, default 75, + Resize the TKinter windows + + Examples + -------- + >>> import emulators + + >>> display = emulators.LcdEmulator() + >>> cc = emulators.CustomCharactersEmulator(display) + + """ + + self.root = tk.Tk() + self.root.title(TITTLE_WINDOWS) + + try: + self.root.winfo_rgb(LCD_BACKGROUND) + self.LCD_BACKGROUND_DEFAULT = LCD_BACKGROUND + self.LCD_BACKGROUND = LCD_BACKGROUND + except tk.TclError: + self.LCD_BACKGROUND_DEFAULT = "green" + self.LCD_BACKGROUND = "green" + print(f"'{LCD_BACKGROUND}' is not a valid LCD_BACKGROUND. List of colors: https://www.tcl-lang.org/man/tcl8.4/TkCmd/colors.htm") + + try: + self.root.winfo_rgb(LCD_FOREGROUND) + self.LCD_FOREGROUND_DEFAULT = LCD_FOREGROUND + self.LCD_FOREGROUND = LCD_FOREGROUND + except tk.TclError: + self.LCD_BACKGROUND_DEFAULT = "black" + self.LCD_BACKGROUND = "black" + print(f"'{LCD_FOREGROUND}' is not a valid LCD_FOREGROUND. List of colors: https://www.tcl-lang.org/man/tcl8.4/TkCmd/colors.htm") + + if SESSION_STATE_BACKLIGHT not in [0, 1]: + print( + f"Error : '{SESSION_STATE_BACKLIGHT}' is not a valid value for SESSION_STATE_BACKLIGHT. Use of 1 by default (ON), use 0 for backlight OFF.") + self.SESSION_STATE_BACKLIGHT = 1 + else: + self.SESSION_STATE_BACKLIGHT = SESSION_STATE_BACKLIGHT + + if FONT in font.families(): + self.FONT = FONT + else: + self.FONT = "Courier" + print(f"'{FONT}' is not a valid FONT, List of fonts: {font.families()} | https://stackoverflow.com/a/64301819") + + if FONT_SIZE > 0: + self.FONT_SIZE = FONT_SIZE + else: + self.FONT_SIZE = 75 + print(f"FONT_SIZE '{FONT_SIZE}' must be higher than 1.") + + if COLUMNS < 1: + print(f"'{COLUMNS}' is not a valid number of COLUMNS, COLUMNS must be higher than 1.") + self.COLUMNS = 16 + else: + self.COLUMNS = COLUMNS + + if ROWS < 1: + print(f"'{ROWS} is not a valid number of ROWS, ROWS must be higher than 1.") + self.ROWS = 2 + else: + self.ROWS = ROWS + + if CHAR_WIDTH < 1: + print(f"'{CHAR_WIDTH} is not a valid number of CHAR_WIDTH, CHAR_WIDTH must be higher than 1.") + self.CHAR_WIDTH = 75 + else: + self.CHAR_WIDTH = CHAR_WIDTH + + self.CHAR_HEIGHT = self.CHAR_WIDTH * 1.6 + self.rectangles = [] + + self.rects = [] + self.texts = [] + + self.canvas = tk.Canvas(self.root, width=self.COLUMNS * self.CHAR_WIDTH, height=self.ROWS * self.CHAR_HEIGHT, + bg=self.LCD_BACKGROUND) + self.canvas.pack() + + font_obj = font.Font(family=self.FONT, size=self.FONT_SIZE) + all_characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=<>?/~`[]{}|\\;:'\",. " + max_char_width = 0 + for char in all_characters: + char_width = font_obj.measure(char) + if char_width > max_char_width: + max_char_width = char_width + if self.CHAR_WIDTH <= max_char_width * 0.95: + self.FONT_SIZE = int(self.CHAR_WIDTH / max_char_width * self.FONT_SIZE * 0.95) + + self.chars = [] + for row in range(self.ROWS): + for col in range(self.COLUMNS): + rect = self.canvas.create_rectangle( + col * self.CHAR_WIDTH, + row * self.CHAR_HEIGHT, + (col + 1) * self.CHAR_WIDTH, + (row + 1) * self.CHAR_HEIGHT, + outline=self.LCD_FOREGROUND, + width=max(1, int(self.CHAR_WIDTH / 15)) + ) + text = self.canvas.create_text( + col * self.CHAR_WIDTH + self.CHAR_WIDTH // 2, + row * self.CHAR_HEIGHT + self.CHAR_HEIGHT // 2, + text="", + font=(self.FONT, self.FONT_SIZE), + fill=self.LCD_FOREGROUND + ) + self.rects.append(rect) + self.texts.append(text) + self.chars.append(text) + + self.custom_characters = CustomCharactersEmulator(self) + + # put string function + def lcd_display_string(self, text, line=0): + line = line - 1 + start_index = line * self.COLUMNS + for i, char in enumerate(text): + if start_index + i < len(self.chars): + self.canvas.itemconfig(self.chars[start_index + i], text=char) + + self.lcd_update() + + # put extended string function. Extended string may contain placeholder like {0xFF} for + # displaying the particular symbol from the symbol table + def lcd_display_extended_string(self, text, line=0): + line = line - 1 + i = 0 + x_offset = 0 + while i < len(text): + match_result = match(r'\{0[xX][0-9a-fA-F]{2}}', text[i:]) + if match_result: + char_code = match_result.group(0) + custom_char_bitmap = self.custom_characters.get_custom_char(char_code) + self.custom_characters.draw_custom_char(custom_char_bitmap, + x_offset * self.CHAR_WIDTH, + line * self.CHAR_HEIGHT, self.LCD_FOREGROUND) + x_offset += 1 + i += 6 + else: + self.canvas.itemconfig(self.chars[line * self.COLUMNS + x_offset], text=text[i]) + x_offset += 1 + i += 1 + self.lcd_update() + + # clear lcd + def lcd_clear(self): + self.root.update_idletasks() + self.root.update() + for i in range(2): + self.lcd_display_string(" ", i) + for rect_id in self.rectangles: + self.canvas.delete(rect_id) + self.rectangles.clear() + + def lcd_update(self): + self.root.update_idletasks() + self.root.update() + + # backlight control (on/off) + # options: lcd_backlight(1) = ON, lcd_backlight(0) = OFF + def lcd_backlight(self, state: int): + if state not in [0, 1]: + print( + f"Error : '{state}' is not a valid value for lcd_backlight(state). Use of 1 by default (ON), use 0 for backlight OFF.") + state = 1 + + if state == 1: + self.LCD_BACKGROUND = self.LCD_BACKGROUND_DEFAULT + self.LCD_FOREGROUND = self.LCD_FOREGROUND_DEFAULT + elif state == 0: + self.LCD_BACKGROUND = self.LCD_FOREGROUND_DEFAULT + self.LCD_FOREGROUND = self.LCD_BACKGROUND_DEFAULT + + self.canvas.configure(bg=self.LCD_BACKGROUND) + + for rect in self.rects: + self.canvas.itemconfig(rect, outline=self.LCD_FOREGROUND) + for text in self.texts: + self.canvas.itemconfig(text, fill=self.LCD_FOREGROUND) + + self.SESSION_STATE_BACKLIGHT = state + + +class CustomCharactersEmulator: + def __init__(self, lcd): + self.lcd = lcd + self.CHAR_WIDTH = self.lcd.CHAR_WIDTH + # Data for custom character #1. Code {0x00} + self.char_1_data = ["11111", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "11111"] + # Data for custom character #2. Code {0x01} + self.char_2_data = ["11111", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "11111"] + # Data for custom character #3. Code {0x02} + self.char_3_data = ["11111", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "11111"] + # Data for custom character #4. Code {0x03} + self.char_4_data = ["11111", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "11111"] + # Data for custom character #5. Code {0x04} + self.char_5_data = ["11111", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "11111"] + # Data for custom character #6. Code {0x05} + self.char_6_data = ["11111", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "11111"] + # Data for custom character #7. Code {0x06} + self.char_7_data = ["11111", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "11111"] + # Data for custom character #8. Code {0x07} + self.char_8_data = ["11111", + "10001", + "10001", + "10001", + "10001", + "10001", + "10001", + "11111"] + + # load custom character data to CG RAM for later use in extended string. Data for + # characters is hold in file custom_characters.txt in the same folder as i2c_dev.py + # file. These custom characters can be used in printing of extended string with a + # placeholder with desired character codes: 1st - {0x00}, 2nd - {0x01}, 3rd - {0x02}, + # 4th - {0x03}, 5th - {0x04}, 6th - {0x05}, 7th - {0x06} and 8th - {0x07}. + def load_custom_characters_data(self): + char_data_list = [ + (f"{{0x00}}", self.char_1_data), + (f"{{0x01}}", self.char_2_data), + (f"{{0x02}}", self.char_3_data), + (f"{{0x03}}", self.char_4_data), + (f"{{0x04}}", self.char_5_data), + (f"{{0x05}}", self.char_6_data), + (f"{{0x06}}", self.char_7_data), + (f"{{0x07}}", self.char_8_data) + ] + + for char_name, bitmap in char_data_list: + if len(bitmap) != 8 or any(len(row) != 5 for row in bitmap): + continue + custom_chars[char_name] = bitmap + + def get_custom_char(self, char_name): + return custom_chars.get(char_name, ["00000"] * 8) + + # Draw CustomCharacters + def draw_custom_char(self, bitmap, x, y, color): + pixel_size = self.CHAR_WIDTH / 5 + for row, line in enumerate(bitmap): + for col, bit in enumerate(line): + if bit == '1': + rect_id = self.lcd.canvas.create_rectangle( + x + (col * pixel_size), + y + (row * pixel_size), + x + ((col + 1) * pixel_size), + y + ((row + 1) * pixel_size), + fill=color, + outline=color + ) + self.lcd.rectangles.append(rect_id) diff --git a/imgs/demo_simulator.gif b/imgs/demo_simulator.gif new file mode 100644 index 0000000..71f410c Binary files /dev/null and b/imgs/demo_simulator.gif differ