feat: Release version 1.2.0 with new features

This commit is contained in:
Eric
2025-09-11 21:38:59 -07:00
parent ce85b4b69b
commit ddd047b2f2
4 changed files with 230 additions and 3 deletions

View File

@@ -31,7 +31,7 @@ You can easily add this component to your project using the idf.py command or by
From your project's root directory, simply run the following command in your terminal:
```bash
idf.py add-dependency "0015/map_tiles^1.1.0"
idf.py add-dependency "0015/map_tiles^1.2.0"
```
This command will automatically add the component to your idf_component.yml file and download the required files the next time you build your project.
@@ -43,7 +43,7 @@ Add to your project's `main/idf_component.yml`:
dependencies:
map_tiles:
git: "https://github.com/0015/map_tiles.git"
version: "^1.1.0"
version: "^1.2.0"
```
### Manual Installation

View File

@@ -1,6 +1,6 @@
namespace: "0015"
name: "map_tiles"
version: "1.1.0"
version: "1.2.0"
maintainers:
- Eric Nam <thatprojectstudio@gmail.com>
description: "Map tiles component for LVGL 9.x - Load and display map tiles with GPS coordinate conversion"

49
script/README.md Normal file
View File

@@ -0,0 +1,49 @@
# **LVGL Map Tile Converter**
This Python script is designed to convert a directory of PNG map tiles into a format compatible with LVGL (Light and Versatile Graphics Library) version 9\. The output is a series of binary files (.bin) with a header and pixel data in the RGB565 format, optimized for direct use with LVGL's image APIs.
The script is multithreaded and can significantly speed up the conversion process for large tile sets.
## **Features**
* **PNG to RGB565 Conversion**: Converts standard 24-bit PNG images into 16-bit RGB565.
* **LVGL v9 Compatibility**: Generates a .bin file with the correct LVGL v9 header format.
* **Multithreaded Conversion**: Utilizes a ThreadPoolExecutor to process tiles in parallel, configurable with the \--jobs flag.
* **Directory Traversal**: Automatically finds and converts all .png files within a specified input directory structure.
* **Skip Existing Files**: Skips conversion for tiles that already exist in the output directory unless the \--force flag is used.
## **Requirements**
The script requires the Pillow library to handle image processing. You can install it using pip:
```bash
pip install Pillow
```
## **Usage**
The script is a command-line tool. You can run it from your terminal with the following arguments:
```bash
python lvgl_map_tile_converter.py --input <input_folder> --output <output_folder> [options]
```
### **Arguments**
* \-i, \--input: **Required**. The root folder containing your map tiles. The script expects a tile structure like zoom/x/y.png.
* \-o, \--output: **Required**. The root folder where the converted .bin tiles will be saved. The output structure will mirror the input: zoom/x/y.bin.
* \-j, \--jobs: **Optional**. The number of worker threads to use for the conversion. Defaults to the number of CPU cores on your system. Using more jobs can speed up the process.
* \-f, \--force: **Optional**. If this flag is set, the script will re-convert all tiles, even if the output .bin files already exist.
### **Examples**
**1\. Basic conversion with default settings:**
```bash
python lvgl_map_tile_converter.py --input ./map_tiles --output ./tiles1
```
**2\. Conversion using a specific number of threads (e.g., 8):**
```bash
python lvgl_map_tile_converter.py --input ./map_tiles --output ./tiles1 --jobs 8
```
**3\. Forcing a full re-conversion:**
```bash
python lvgl_map_tile_converter.py --input ./map_tiles --output ./tiles1 --force
```

View File

@@ -0,0 +1,178 @@
import os
import struct
import argparse
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from PIL import Image # pip install pillow
# No implicit defaults; these are set from CLI in main()
INPUT_ROOT = None
OUTPUT_ROOT = None
# Convert RGB to 16-bit RGB565
def to_rgb565(r, g, b):
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
# Strip .png or .bin extensions
def clean_tile_name(filename):
name = filename
while True:
name, ext = os.path.splitext(name)
if ext.lower() not in [".png", ".bin", ".jpg", ".jpeg"]:
break
filename = name
return filename
# Create LVGL v9-compatible .bin image
def make_lvgl_bin(png_path, bin_path):
im = Image.open(png_path).convert("RGB")
w, h = im.size
pixels = im.load()
stride = (w * 16 + 7) // 8 # bytes per row (RGB565 = 16 bpp)
flags = 0x00 # no compression, no premult
color_format = 0x12 # RGB565
magic = 0x19
header = bytearray()
header += struct.pack("<B", magic)
header += struct.pack("<B", color_format)
header += struct.pack("<H", flags)
header += struct.pack("<H", w)
header += struct.pack("<H", h)
header += struct.pack("<H", stride)
header += struct.pack("<H", 0) # reserved
body = bytearray()
for y in range(h):
for x in range(w):
r, g, b = pixels[x, y]
rgb565 = to_rgb565(r, g, b)
body += struct.pack("<H", rgb565)
os.makedirs(os.path.dirname(bin_path), exist_ok=True)
if os.path.isdir(bin_path):
# In case a folder mistakenly exists where a file should
print(f"[Fix] Removing incorrect folder: {bin_path}")
os.rmdir(bin_path)
with open(bin_path, "wb") as f:
f.write(header)
f.write(body)
print(f"[OK] {png_path}{bin_path}")
# Yield (input_path, output_path) pairs for all PNG tiles under INPUT_ROOT
def _iter_tile_paths():
for zoom in sorted(os.listdir(INPUT_ROOT)):
zoom_path = os.path.join(INPUT_ROOT, zoom)
if not os.path.isdir(zoom_path) or not zoom.isdigit():
continue
for x_tile in sorted(os.listdir(zoom_path)):
x_path = os.path.join(zoom_path, x_tile)
if not os.path.isdir(x_path) or not x_tile.isdigit():
continue
for y_file in sorted(os.listdir(x_path)):
if not y_file.lower().endswith(".png"):
continue
tile_base = clean_tile_name(y_file)
input_path = os.path.join(INPUT_ROOT, zoom, x_tile, y_file)
output_path = os.path.join(OUTPUT_ROOT, zoom, x_tile, f"{tile_base}.bin")
yield input_path, output_path
def convert_all_tiles(jobs=1, force=False):
"""
Convert tiles with optional threading.
- jobs: number of worker threads (>=1)
- force: if True, re-generate even if output exists
"""
if not os.path.isdir(INPUT_ROOT):
print(f"[ERROR] '{INPUT_ROOT}' not found.")
return
# Build task list (skip existing unless --force)
tasks = []
for input_path, output_path in _iter_tile_paths():
if not force and os.path.isfile(output_path):
print(f"[Skip] {output_path}")
continue
tasks.append((input_path, output_path))
if not tasks:
print("[INFO] Nothing to do.")
return
print(f"[INFO] Converting {len(tasks)} tiles with {jobs} thread(s)...")
if jobs <= 1:
# Serial path
for inp, outp in tasks:
try:
make_lvgl_bin(inp, outp)
except Exception as e:
print(f"[Error] Failed to convert {inp}{e}")
return
# Threaded path
print_lock = threading.Lock()
with ThreadPoolExecutor(max_workers=jobs) as ex:
future_map = {ex.submit(make_lvgl_bin, inp, outp): (inp, outp) for inp, outp in tasks}
for fut in as_completed(future_map):
inp, outp = future_map[fut]
try:
fut.result()
except Exception as e:
with print_lock:
print(f"[Error] Failed to convert {inp}{e}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Convert OSM PNG tiles into LVGL-friendly .bin files (RGB565).",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-i", "--input",
required=True,
default=argparse.SUPPRESS, # hide '(default: None)'
help="Input root folder containing tiles in zoom/x/y.png structure",
)
parser.add_argument(
"-o", "--output",
required=True,
default=argparse.SUPPRESS, # hide '(default: None)'
help="Output root folder where .bin tiles will be written",
)
parser.add_argument(
"-j", "--jobs",
type=int,
default=os.cpu_count(),
help="Number of worker threads",
)
parser.add_argument(
"-f", "--force",
action="store_true",
help="Rebuild even if output file already exists",
)
args = parser.parse_args()
# Basic checks
if not os.path.isdir(args.input):
parser.error(f"Input folder not found or not a directory: {args.input}")
os.makedirs(args.output, exist_ok=True)
# Apply CLI values
INPUT_ROOT = args.input
OUTPUT_ROOT = args.output
convert_all_tiles(jobs=max(1, args.jobs), force=args.force)