Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added fonts/Inter-Regular.ttf
Binary file not shown.
4 changes: 3 additions & 1 deletion include/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
#include "layout.h"
#define SCREEN_W LY_W
#define SCREEN_H LY_H
#define BACKLIGHT_PIN TFT_BL // set by build flags per board
#ifndef BACKLIGHT_PIN
#define BACKLIGHT_PIN -1 // set via build_flags -D BACKLIGHT_PIN=<pin>
#endif
#define BACKLIGHT_CH 0
#define BACKLIGHT_FREQ 5000
#define BACKLIGHT_RES 8
Expand Down
30 changes: 30 additions & 0 deletions include/fonts.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef FONTS_H
#define FONTS_H

#include <LovyanGFX.hpp>
#include "fonts/inter_10.h"
#include "fonts/inter_14.h"
#include "fonts/inter_19.h"

// Smooth VLW font identifiers — values match the old bitmap font numbers
// so existing logic (e.g. compact ? FONT_LARGE : FONT_LARGE) stays readable.
enum FontID : uint8_t {
FONT_SMALL = 1, // Inter 10pt (was bitmap Font 1, 8px GLCD)
FONT_BODY = 2, // Inter 14pt (was bitmap Font 2, 16px)
FONT_LARGE = 4, // Inter 19pt (was bitmap Font 4, 26px)
FONT_7SEG = 7, // Built-in 7-segment (kept for clock displays)
};

inline void setFont(lgfx::LGFX_Device& tft, FontID id) {
switch (id) {
case FONT_SMALL: tft.loadFont(inter_10); break;
case FONT_BODY: tft.loadFont(inter_14); break;
case FONT_LARGE: tft.loadFont(inter_19); break;
case FONT_7SEG:
tft.unloadFont();
tft.setTextFont(7);
break;
}
}

#endif // FONTS_H
414 changes: 414 additions & 0 deletions include/fonts/inter_10.h

Large diffs are not rendered by default.

603 changes: 603 additions & 0 deletions include/fonts/inter_14.h

Large diffs are not rendered by default.

898 changes: 898 additions & 0 deletions include/fonts/inter_19.h

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions partitions_4mb_c3.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Partition table for 4 MB flash (ESP32-C3 Super Mini)
# app partitions are 1.875 MB each — gives ~630 KB headroom over current firmware.
# SPIFFS is not used by BambuHelper (settings stored in NVS); kept small for padding.
# NOTE: changing the partition table requires a full USB flash; OTA cannot update it.
#
# Name, Type, SubType, Offset, Size,
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1E0000,
app1, app, ota_1, 0x1F0000, 0x1E0000,
spiffs, data, spiffs, 0x3D0000, 0x30000,
79 changes: 14 additions & 65 deletions platformio.ini
Original file line number Diff line number Diff line change
@@ -1,28 +1,15 @@
; BambuHelper - Bambu Lab Printer Monitor
; Supports ESP32-S3 Super Mini and ESP32-2432S028 (CYD)
; Supports ESP32-S3 Super Mini, ESP32-2432S028 (CYD), ESP32-C3 Super Mini

; --- Shared library dependencies ---
[common]
lib_deps =
bodmer/TFT_eSPI@^2.5.43
bodmer/TFT_eWidget@^0.0.6
lovyan03/LovyanGFX@^1.1.16
knolleary/PubSubClient@^2.8
bblanchon/ArduinoJson@^7.0

; --- Shared TFT_eSPI + font flags (driver/pins are per-env) ---
common_flags =
-D USER_SETUP_LOADED=1
-D DISABLE_ALL_LIBRARY_WARNINGS=1
-D LOAD_GLCD=1
-D LOAD_FONT2=1
-D LOAD_FONT4=1
-D LOAD_FONT6=1
-D LOAD_FONT7=1
-D LOAD_GFXFF=1
-D SMOOTH_FONT=1

; =============================================================================
; ESP32-S3 Super Mini + ST7789 240x240
; ESP32-S3 Super Mini + ST7789 240x240
; =============================================================================
[env:esp32s3]
platform = espressif32
Expand All @@ -33,65 +20,39 @@ board_build.partitions = partitions_4mb.csv
board_build.arduino.memory_type = qio_qspi
lib_deps = ${common.lib_deps}
build_flags =
${common.common_flags}
-D BOARD_VARIANT=\"esp32s3\"
-D BOARD_IS_S3=1
-D ENABLE_OTA_AUTO=1
-D SPI_FREQUENCY=40000000
; --- Display driver ---
-D ST7789_DRIVER=1
-D TFT_WIDTH=240
-D TFT_HEIGHT=240
-D BACKLIGHT_PIN=13
; --- USB Serial (S3 native USB CDC) ---
-D ARDUINO_USB_CDC_ON_BOOT=1
; --- SPI pins (S3 Super Mini) ---
-D TFT_MOSI=11 ;SDA
-D TFT_SCLK=12 ;SCL
-D TFT_CS=10
-D TFT_DC=9
-D TFT_RST=8 ;14
-D TFT_BL=13
-D USE_FSPI_PORT=1

; =============================================================================
; ESP32-2432S028 (CYD - Cheap Yellow Display) + ILI9341 240x320
; ESP32-2432S028 (CYD - Cheap Yellow Display) + ILI9342 240x320
; =============================================================================
[env:cyd]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
upload_speed = 230400
board_build.partitions = partitions_4mb.csv
board_build.partitions = min_spiffs.csv
lib_deps =
${common.lib_deps}
https://github.com/PaulStoffregen/XPT2046_Touchscreen.git
build_flags =
${common.common_flags}
-D SPI_FREQUENCY=27000000
; --- Display driver ---
; ILI9341_2 works for both ILI9341 and ILI9342 CYD variants
-D ILI9341_2_DRIVER=1
-D TFT_INVERSION_ON=1
-D TFT_WIDTH=240
-D TFT_HEIGHT=320
; --- CYD display SPI pins ---
-D TFT_MOSI=13
-D TFT_SCLK=14
-D TFT_CS=15
-D TFT_DC=2
-D TFT_RST=12
-D TFT_BL=21
; --- CYD touch (XPT2046 on separate SPI bus) ---
-D USE_XPT2046=1
-D TOUCH_CS=33
-D TOUCH_IRQ=36
-D TOUCH_MOSI=32
-D TOUCH_MISO=39
-D TOUCH_CLK=25
-D SPI_TOUCH_FREQUENCY=2500000
-D USE_XPT2046=1
; --- Layout profile ---
-D DISPLAY_CYD=1
-D BOARD_VARIANT=\"cyd\"
-D BACKLIGHT_PIN=21

; =============================================================================
; ESP32-C3 Super Mini + ST7789 240x240
Expand All @@ -101,24 +62,12 @@ platform = espressif32
board = lolin_c3_mini
framework = arduino
monitor_speed = 115200
board_build.partitions = partitions_4mb.csv
extra_scripts = pre:scripts/patch_spi_for_esp32-c3.py
board_build.partitions = partitions_4mb_c3.csv
lib_deps = ${common.lib_deps}
build_flags =
${common.common_flags}
-D SPI_FREQUENCY=40000000
; --- Display driver ---
-D ST7789_DRIVER=1
-D TFT_WIDTH=240
-D TFT_HEIGHT=240
; --- USB Serial (C3 native USB CDC) ---
-D ARDUINO_USB_CDC_ON_BOOT=1
; --- SPI pins (C3 Super Mini) ---
-D TFT_MOSI=20 ;SDA
-D TFT_SCLK=21 ;SCL
-D TFT_CS=6
-D TFT_DC=7
-D TFT_RST=10
-D TFT_BL=5
-D BOARD_VARIANT=\"esp32c3\"
-D BOARD_IS_C3=1
-D ENABLE_OTA_AUTO=1
-D BACKLIGHT_PIN=5
; --- USB Serial (C3 native USB CDC) ---
-D ARDUINO_USB_CDC_ON_BOOT=1
156 changes: 156 additions & 0 deletions scripts/generate_vlw_fonts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Generate TFT_eSPI-compatible VLW font PROGMEM headers from a TTF file.

VLW format (Bodmer/TFT_eSPI smooth font):
Header: glyph_count(u32) | version(u32=6) | font_size(u32) | padding(u32)
ascent(u32) | descent(u32)
Per glyph metrics (24 bytes each):
unicode(u32) | height(u32) | width(u32) | advance(u32)
top_offset(u32) | left_offset(u32)
Glyph bitmaps: 8-bit alpha, row-major, width*height bytes each.

Usage:
python scripts/generate_vlw_fonts.py
"""

import struct
import sys
from pathlib import Path

try:
import freetype
except ImportError:
print("Install freetype-py: pip install freetype-py")
sys.exit(1)

# Character set: printable ASCII + degree symbol
CHARSET = list(range(0x20, 0x7F)) + [0xB0]

# Font sizes to generate: (name, pixel_size)
# Tuned to match TFT_eSPI bitmap font metrics while fitting flash budget
FONTS = [
("inter_10", 10),
("inter_14", 14),
("inter_19", 19),
]

TTF_PATH = Path(__file__).parent.parent / "fonts" / "Inter-Regular.ttf"
OUT_DIR = Path(__file__).parent.parent / "include" / "fonts"


def generate_vlw(ttf_path: str, pixel_size: int) -> bytes:
"""Generate VLW binary data for the given font and size."""
face = freetype.Face(str(ttf_path))
face.set_pixel_sizes(0, pixel_size)

glyphs = []
for codepoint in CHARSET:
face.load_char(chr(codepoint), freetype.FT_LOAD_RENDER)
g = face.glyph
bmp = g.bitmap

width = bmp.width
height = bmp.rows
advance = g.advance.x >> 6 # 26.6 fixed point to pixels
top_offset = g.bitmap_top
left_offset = g.bitmap_left

# Extract 8-bit alpha bitmap
if width > 0 and height > 0:
alpha = bytes(bmp.buffer)
# Handle pitch != width (padding per row)
if bmp.pitch != width:
alpha = b""
for row in range(height):
start = row * bmp.pitch
alpha += bytes(bmp.buffer[start:start + width])
else:
alpha = b""

glyphs.append({
"unicode": codepoint,
"height": height,
"width": width,
"advance": advance,
"top_offset": top_offset,
"left_offset": left_offset,
"bitmap": alpha,
})

# Compute font metrics
ascent = face.size.ascender >> 6
descent = -(face.size.descender >> 6) # TFT_eSPI expects positive descent

# Build VLW binary
buf = bytearray()

# Header (6 x uint32 big-endian)
glyph_count = len(glyphs)
buf += struct.pack(">I", glyph_count)
buf += struct.pack(">I", 6) # version
buf += struct.pack(">I", pixel_size)
buf += struct.pack(">I", 0) # padding
buf += struct.pack(">I", ascent)
buf += struct.pack(">I", descent)

# Glyph metrics table (28 bytes each — 7 x uint32)
# TFT_eSPI reads: unicode, height, width, xAdvance, dY, dX, padding(ignored)
for g in glyphs:
buf += struct.pack(">I", g["unicode"])
buf += struct.pack(">I", g["height"])
buf += struct.pack(">I", g["width"])
buf += struct.pack(">I", g["advance"])
buf += struct.pack(">i", g["top_offset"])
buf += struct.pack(">i", g["left_offset"])
buf += struct.pack(">I", 0) # padding — TFT_eSPI reads and discards

# Glyph bitmaps (sequential, same order as metrics)
for g in glyphs:
buf += g["bitmap"]

return bytes(buf)


def vlw_to_header(name: str, vlw_data: bytes) -> str:
"""Convert VLW binary to a C PROGMEM header."""
lines = [
f"// Auto-generated VLW font: {name}",
f"// Size: {len(vlw_data)} bytes ({len(vlw_data) / 1024:.1f} KB)",
"#pragma once",
"#include <pgmspace.h>",
"",
f"const uint8_t {name}[] PROGMEM = {{",
]

# Emit bytes, 16 per line
for i in range(0, len(vlw_data), 16):
chunk = vlw_data[i:i + 16]
hex_vals = ", ".join(f"0x{b:02X}" for b in chunk)
lines.append(f" {hex_vals},")

lines.append("};")
lines.append("")
return "\n".join(lines)


def main():
if not TTF_PATH.exists():
print(f"TTF not found: {TTF_PATH}")
sys.exit(1)

OUT_DIR.mkdir(parents=True, exist_ok=True)

for name, size in FONTS:
print(f"Generating {name} ({size}px)...", end=" ")
vlw_data = generate_vlw(str(TTF_PATH), size)
header = vlw_to_header(name, vlw_data)

out_path = OUT_DIR / f"{name}.h"
out_path.write_text(header, encoding="utf-8")
print(f"{len(vlw_data)} bytes -> {out_path}")

print("Done.")


if __name__ == "__main__":
main()
7 changes: 4 additions & 3 deletions src/clock_mode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "settings.h"
#include "config.h"
#include "layout.h"
#include "fonts.h"
#include <time.h>

static int prevMinute = -1;
Expand Down Expand Up @@ -41,13 +42,13 @@ void drawClock() {
snprintf(timeBuf, sizeof(timeBuf), "%2d:%02d", h, now.tm_min);
}
tft.setTextDatum(MC_DATUM);
tft.setTextFont(7);
setFont(tft, FONT_7SEG);
tft.setTextColor(CLR_TEXT, bg);
tft.drawString(timeBuf, LY_W / 2, LY_CLK_TIME_Y);

// AM/PM indicator for 12h mode
if (!netSettings.use24h) {
tft.setTextFont(4);
setFont(tft, FONT_LARGE);
tft.setTextColor(CLR_TEXT_DIM, bg);
tft.drawString(now.tm_hour < 12 ? "AM" : "PM", LY_W / 2, LY_CLK_AMPM_Y);
}
Expand All @@ -65,7 +66,7 @@ void drawClock() {
case 5: snprintf(dateBuf, sizeof(dateBuf), "%s %s %d, %04d", days[now.tm_wday], months[now.tm_mon], day, year); break;
default: snprintf(dateBuf, sizeof(dateBuf), "%s %02d.%02d.%04d", days[now.tm_wday], day, mon, year); break;
}
tft.setTextFont(4);
setFont(tft, FONT_LARGE);
tft.setTextColor(CLR_TEXT_DIM, bg);
tft.drawString(dateBuf, LY_W / 2, LY_CLK_DATE_Y);
}
Loading