Pimoroni Explorer Kit Tutorial

Thanks for posting, that looks excellent! I would love to see the solution and/or code.


looks really cool!

I like to green-ish glowing fonts


That is a very neat display. I’d like to see the code and know how to access the font16.py font.

1 Like

Sorry. Because it was my first post, only one picture upload was allowed. Here are the modifications at the backside of the Pimoroni Explorer.

In the upper-right corner, there is the Real Time Clock DS3231 at the second I2C connector. This was the most important part for me.

The LiPo battery is located in the lower section. Above it, you can see a charger/step-up converter TP4056. These parts are optional because the Explorer can be powered by an external 5V power supply, allowing it to operate independently.

Here is the modified code for the Pimoroni Explorer with Sensor Stick, RTC, and nicer fonts.

# Pimoroni Explorer Board, Analog/Digital Clock
# Tony Goodhew, Leicester UK - 6 Nov 2024
# Extended by Hans Gloor, Switzerland - 06.03.2026
# PIM745 Sensor Stick, added: DS3231 RTC
# Optional Date/Time Setup on Boot <<<<
# New fonts with Font Converter (*.ttf >>> *.py)
# 7-seg font (digital time), comic font (clock name)

from explorer import (
    display, i2c,
    button_a, button_b, button_c,
    button_x, button_y, button_z,
    BLACK, WHITE, GREEN, RED, BLUE, YELLOW, CYAN
)
# ORANGE = display.create_pen(255, 095, 000)	# RGB definition

from breakout_bme280 import BreakoutBME280
from breakout_ltr559 import BreakoutLTR559
import math
import time
import font7		# 7-seg. font
import font66		# Comic font
import font12
import font16b		#b for bold
import font24b
from fontdraw import draw_text		# font renderer

# RTC (DS3231)	================================================

RTC_ADDR = 0x68

def bcd_to_int(x):
    return (x >> 4) * 10 + (x & 0x0F)

def int_to_bcd(x):
    return ((x // 10) << 4) | (x % 10)

def rtc_read():
    try:
        raw = i2c.readfrom_mem(RTC_ADDR, 0x00, 7)
        sec   = bcd_to_int(raw[0] & 0x7F)
        minute= bcd_to_int(raw[1])
        hour  = bcd_to_int(raw[2] & 0x3F)
        wday  = (bcd_to_int(raw[3]) - 1) % 7
        day   = bcd_to_int(raw[4])
        month = bcd_to_int(raw[5] & 0x1F)
        year  = 2000 + bcd_to_int(raw[6])
        return hour, minute, sec, day, month, wday, year
    except:
        return None

def rtc_write(h, m, s, day, month, wd, year):
    data = bytearray(7)
    data[0] = int_to_bcd(s)
    data[1] = int_to_bcd(m)
    data[2] = int_to_bcd(h)
    data[3] = int_to_bcd(wd + 1)
    data[4] = int_to_bcd(day)
    data[5] = int_to_bcd(month)
    data[6] = int_to_bcd(year - 2000)
    i2c.writeto_mem(RTC_ADDR, 0x00, data)

# CONSTANTS	===================================================

YEAR = 2026
prev_z = 1
p_slope = 1.0462
p_shift = 0

days = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"]
months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
MAX_DAYS = [31,28,31,30,31,30,31,31,30,31,30,31]

display.set_layer(0)
display.set_pen(BLACK)
display.clear()
display.set_backlight(1.0)
display.set_layer(1)
display.set_pen(BLACK)
display.clear()

# RTC → Initial Values	========================================

rtc = rtc_read()
if rtc:
    h, m, s, day, month, wd, YEAR = rtc
else:
    h, m, s = 12, 0, 0
    day, month, wd = 1, 1, 0

# BOOT SCREEN – SETUP OPTION    ===============================

enter_setup = False
t0 = time.ticks_ms()

while time.ticks_diff(time.ticks_ms(), t0) < 4000:		# Wait time !!
    display.set_pen(BLACK)
    display.clear()
    draw_text(display, "PIMORONI Explorer",10,10, font24b, GREEN , 2)
    draw_text(display, "Multi-Sensor Stick & Clock",10,50, font16b, CYAN , 2)
    draw_text(display, "Raspberry Pi Pico 2 with RP2350B",10,86, font12, BLUE , 2)
    draw_text(display, "Display 2.8 inch with 320x240 px",10,110, font12, BLUE , 2)
    draw_text(display, "Clock starting in 4 sec",36,148, font16b, RED , 2)
    draw_text(display, "Press C for SETUP",60,185, font16b, YELLOW , 2)
    draw_text(display, "   <",10,185, font16b, YELLOW , 2)
    display.update()
    time.sleep(0.3)
    display.set_pen(BLACK)
    display.rectangle(10,185,50,18)
    draw_text(display, "  < ",10,185, font16b, YELLOW , 2)
    display.update()
    time.sleep(0.3)
    display.set_pen(BLACK)
    display.rectangle(10,185,50,18)
    draw_text(display, " <  ",10,185, font16b, YELLOW , 2)
    display.update()
    time.sleep(0.3)
    display.set_pen(BLACK)
    display.rectangle(10,185,50,18)
    draw_text(display, "<   ",10,185, font16b, YELLOW , 2)
    display.update()
    time.sleep(0.3)    
    display.set_pen(BLACK)
    display.rectangle(10,185,35,18)
    display.update()

    if button_c.value() == 0:
        enter_setup = True
        time.sleep(0.3)
        break

# SETUP (only if requested)	===================================

if enter_setup:

    # TIME SETUP	------------------
    while True:
        display.set_pen(BLACK)
        display.clear()

        display.set_pen(YELLOW)
        display.text("SET TIME", 80, 10, 320, 3)

        display.set_pen(WHITE)
        display.text(f"{h:02}:{m:02}:{s:02}", 85, 70, 320, 4)

        display.set_pen(CYAN)
        display.text("A/X : Hour", 10, 150, 320, 2)
        display.text("B/Y : Minute", 10, 170, 320, 2)
        display.text("C : Sec = 0", 10, 190, 320, 2)
        display.text("Z : NEXT", 10, 210, 320, 2)

        display.update()

        if button_a.value() == 0:
            h = (h - 1) % 24
            time.sleep(0.2)
        if button_x.value() == 0:
            h = (h + 1) % 24
            time.sleep(0.2)
        if button_b.value() == 0:
            m = (m - 1) % 60
            time.sleep(0.2)
        if button_y.value() == 0:
            m = (m + 1) % 60
            time.sleep(0.2)
        if button_c.value() == 0:
            s = 0
            time.sleep(0.2)

        z = button_z.value()
        if prev_z == 1 and z == 0:
            prev_z = z
            break
        prev_z = z

    # DATE SETUP    ---------------
    while True:
        display.set_pen(BLACK)
        display.clear()

        display.set_pen(YELLOW)
        display.text("SET DATE", 80, 10, 320, 3)

        display.set_pen(WHITE)
        display.text(f"{day} {months[month-1]}", 90, 70, 320, 4)
        display.text(days[wd], 90, 120, 320, 2)

        display.set_pen(CYAN)
        display.text("A/X : Day", 10, 150, 320, 2)
        display.text("B/Y : Month", 10, 170, 320, 2)
        display.text("C : Weekday", 10, 190, 320, 2)
        display.text("Z : START", 10, 210, 320, 2)
        display.update()

        if button_a.value() == 0:
            day = day - 1 if day > 1 else MAX_DAYS[month-1]
            time.sleep(0.2)
        if button_x.value() == 0:
            day = day + 1 if day < MAX_DAYS[month-1] else 1
            time.sleep(0.2)
        if button_b.value() == 0:
            month = 12 if month == 1 else month - 1
            time.sleep(0.2)
        if button_y.value() == 0:
            month = 1 if month == 12 else month + 1
            time.sleep(0.2)
        if button_c.value() == 0:
            wd = (wd + 1) % 7
            time.sleep(0.2)

        z = button_z.value()
        if prev_z == 1 and z == 0:
            prev_z = z
            break
        prev_z = z

    rtc_write(h, m, s, day, month, wd, YEAR)

# PSEUDO RTC BASE	=============================================

start_h, start_m, start_s = h, m, s
start_ticks = time.ticks_ms()

# SENSOR INIT	=================================================

bme = BreakoutBME280(i2c, address=0x76)
ltr = BreakoutLTR559(i2c)

# DRAWING SETUP	===============================================

def hand(ang, long):
    ang -= 90
    ls = long / 10
    x0 = int(long * math.cos(math.radians(ang))) + cx
    y0 = int(long * math.sin(math.radians(ang))) + cy
    x1 = int(ls * math.cos(math.radians(ang + 90))) + cx
    y1 = int(ls * math.sin(math.radians(ang + 90))) + cy
    x2 = int(ls * math.cos(math.radians(ang - 90))) + cx
    y2 = int(ls * math.sin(math.radians(ang - 90))) + cy
    display.triangle(x0,y0,x1,y1,x2,y2)
    display.circle(cx,cy,int(ls))

display.set_layer(0)
display.set_pen(BLACK)
display.clear()
display.set_layer(1)
display.set_pen(BLACK)
display.clear()

cx, cy = 199, 119
l, ls = 110, 85
#display.set_pen(BLUE)			# choice: blue ring
display.circle(cx,cy,110)		# or black
#display.set_pen(BLACK)			# or circle only
#display.circle(cx,cy,108)		# delete inner part
display.update()

display.set_pen(WHITE)
for a in range(0,360,6):
    display.line(cx,cy,
        cx + int(l * math.cos(math.radians(a))),
        cy + int(l * math.sin(math.radians(a))))

display.set_pen(BLACK)
display.circle(cx,cy,100)

display.set_pen(RED)
for a in range(0,360,30):
    display.line(cx,cy,
        cx + int(l * math.cos(math.radians(a))),
        cy + int(l * math.sin(math.radians(a))))

display.set_pen(WHITE)
draw_text(display, "Temperature", 5, 40, font12, WHITE, 1)
draw_text(display, "Humidity", 5, 90, font12, WHITE, 1)
draw_text(display, "Brightness", 5, 142, font12, WHITE, 1)
#draw_text(display, "Illuminance", 5, 142, font12, WHITE, 1)	# Phys. correctly
draw_text(display, "Air Pressure", 5, 196, font12, WHITE, 1)

# MAIN LOOP	===================================================

while True:
    elapsed = time.ticks_diff(time.ticks_ms(), start_ticks) // 1000

    s = (start_s + elapsed) % 60
    m = (start_m + (start_s + elapsed)//60) % 60
    h = (start_h + (start_m + (start_s + elapsed)//60)//60) % 24
    
    display.set_pen(BLACK)
    display.circle(cx,cy,93)
    
    #draw_text(display, "Hans Gloor", 154,68, font66, GREEN, 0)
    draw_text(display, "RT-Clock", 150,68, font66, GREEN, 2)
    display.set_pen(YELLOW)
    draw_text(display, "12",180,34, font24b, YELLOW , 2)
    draw_text(display, "3",268,105, font24b, YELLOW , 2)
    draw_text(display, "6",192,180, font24b, YELLOW , 2)
    draw_text(display, "9",118,105, font24b, YELLOW , 2)
    
    display.set_pen(BLUE)				# Background for day and date
    display.rectangle(164,150,72,25)
    draw_text(display,days[wd],166,150,font12, WHITE, 0)
    draw_text(display,f"{day}. {months[month-1]}", 174, 161, font12, WHITE, 0)

    mang = int((m + s/60)*6)
    hang = int((h + m/60)*30)

    display.set_pen(BLUE)				# Minutes
    hand(mang,90)
    display.set_pen(RED)				# Hours
    hand(hang,70)

    sang = 6*s - 90
    display.set_pen(CYAN)				# Seconds
    display.line(cx,cy,
        cx + int(ls * math.cos(math.radians(sang))),
        cy + int(ls * math.sin(math.radians(sang))))
    display.line(cx+1,cy+1,
        cx+1 + int(ls * math.cos(math.radians(sang))),
        cy+1 + int(ls * math.sin(math.radians(sang))))    
    display.circle(cx,cy,3)

    dt = f"{h:02}:{m:02}:{s:02}"

    display.set_pen(BLACK)				# deletes areas for:
    display.rectangle(5,10,127,18)		# time, 7-seg.
    display.rectangle(5,56,90,18)		# Temperature
    display.rectangle(5,104,80,18)		# Humidity
    display.rectangle(5,158,82,18)		# Brightness
    display.rectangle(5,210,115,18)		# Pressure

    draw_text(display, dt, 4, 10, font7, RED, 0)		# Digital clock
    display.set_pen(GREEN)
    temperature, pressure, humidity = bme.read()
    draw_text(display, f"{round(temperature,1)} °C", 5, 58, font16b, GREEN, 0)
    draw_text(display, f"{int(humidity)} %", 5, 106, font16b, GREEN, 0)
    reading = ltr.get_reading()
    display.set_pen(BLACK)
    draw_text(display, f"{str(int(reading[-1]))} Lux", 5, 160, font16b, GREEN, 0)
    draw_text(display, f"{int(p_slope*(pressure/100)+p_shift)} hPa", 5, 212, font16b, GREEN, 0)

    display.update()
    time.sleep(0.1)
    

This code runs when all text output uses the original Pimoroni fonts (see Tony’s listing). Of course the imports of the renderer and special fonts must be commented out.

If you want to achieve the more attractive text output, you’ll need to tackle the more involved part of the exercise. We’re using my font converter, which runs on standard Python (e.g., version 3.14). Here’s the code for it.

import os
import math
from PIL import Image, ImageDraw, ImageFont

# =====================
# SETUP Font Converter
# =====================
#FONT_PATH = r"C:\Windows\Fonts\DejaVuSans.ttf"
#FONT_PATH = r"C:\! Pi Pico\!Fonts\www\comici.ttf"
FONT_PATH = r"C:\Windows\Fonts\Consola.ttf"
OUTPUT_DIR = r"C:\! Pi Pico\!Fonts\py-fonts"

FONT_SIZE = 10      # 10, 12, 20, 16, 24, etc
ASCII_START = 32
ASCII_END   = 126    # 126 !!!!!!!!
EXTRA_CHARS = "°%"     # define: e.g.: Ă€Ă„Ă¶Ă–ĂŒĂœ ° %
extra_codes = [ord(c) for c in EXTRA_CHARS]
all_codes = list(range(ASCII_START, ASCII_END + 1)) + extra_codes

MONOSPACE = False   #True = Mono, False = proportional
#MONOSPACE = True
# =====================

os.makedirs(OUTPUT_DIR, exist_ok=True)

font = ImageFont.truetype(FONT_PATH, FONT_SIZE)

# --- Echte Fontmetriken ---
ascent, descent = font.getmetrics()
HEIGHT = ascent + descent   # baseline-stabile Höhe

# --- Maximale Breite bestimmen ---
max_width = 0
for code in all_codes:
    char = chr(code)
    bbox = font.getbbox(char)
    width = bbox[2] - bbox[0]
    if width > max_width:
        max_width = width

WIDTH = max_width if MONOSPACE else None

# --- Glyphen erzeugen ---
font_dict = {}

for code in all_codes:
    char = chr(code)

    bbox = font.getbbox(char)
    char_width = bbox[2] - bbox[0]

    glyph_width = WIDTH if MONOSPACE else char_width

    img = Image.new("1", (glyph_width, HEIGHT), 0)
    draw = ImageDraw.Draw(img)

    # Baseline korrekt: KEIN Trimmen!
    draw.text((0, 0), char, font=font, fill=1)

    glyph_rows = []

    for y in range(HEIGHT):
        value = 0
        for x in range(glyph_width):
            if img.getpixel((x, y)):
                value |= (1 << (glyph_width - 1 - x))
        glyph_rows.append(value)

    font_dict[code] = (glyph_width, glyph_rows)

# --- Bytes berechnen ---
if MONOSPACE:
    BYTES_PER_ROW = math.ceil(WIDTH / 8)
else:
    widest = max(w for w, _ in font_dict.values())
    BYTES_PER_ROW = math.ceil(widest / 8)

HEX_DIGITS = BYTES_PER_ROW * 2

# --- Dateiname ---
DICT_NAME = f"font{FONT_SIZE}"
OUTPUT_FILE = os.path.join(OUTPUT_DIR, f"{DICT_NAME}.py")

# --- Datei schreiben ---
with open(OUTPUT_FILE, "w") as f:

    f.write(f"# Auto-generated font from {os.path.basename(FONT_PATH)}\n")
    f.write(f"# Font size: {FONT_SIZE}\n")
    f.write(f"# Height (ascent+descent): {HEIGHT}\n")
    f.write(f"# Monospace: {MONOSPACE}\n")
    f.write(f"# Bytes per row (max): {BYTES_PER_ROW}\n\n")

    f.write(f"HEIGHT = {HEIGHT}\n")
    if MONOSPACE:
        f.write(f"WIDTH  = {WIDTH}\n")
    f.write("\n")

    f.write("FONT = {\n")

    for code, (width, rows) in font_dict.items():
        f.write(f"    {code}: (\n")
        f.write(f"        {width},\n")
        f.write("        [\n")
        for row in rows:
            f.write(f"            0x{row:0{HEX_DIGITS}X},\n")
        f.write("        ]\n")
        f.write("    ),\n")

    f.write("}\n")

print("Font created:", OUTPUT_FILE)

Please review the setup section carefully and specify your requirements, such as font type, font size, etc., as well as the source and destination directories. You will receive a fontxx.py file that can be used with the MicroPython renderer.

Here is the Micro Python code for the renderer called fontdraw.py. This code and the generated fonts should be copied to the \lib directory on the RP2350B. The required modules are then imported from here. I use Thonny as editor.

# ==========================================
# Universeller Renderer fĂŒr generierte Fonts
# Row-orientiertes Bitformat
# ==========================================

def draw_char(display, ch, x, y, font, color):
    display.set_pen(color)

    # Fallback auf Space
    width, glyph = font.FONT.get(ord(ch), font.FONT[32])

    for row_index, bits in enumerate(glyph):
        for col in range(width):
            if bits & (1 << (width - 1 - col)):
                display.pixel(x + col, y + row_index)


def draw_text(display, text, x, y, font, color, spacing=1):
    """
    spacing = zusÀtzlicher Abstand zwischen Zeichen
    """
    cx = x

    for ch in text:
        width, _ = font.FONT.get(ord(ch), font.FONT[32])
        draw_char(display, ch, cx, y, font, color)
        cx += width + spacing


def text_width(text, font, spacing=1):
    """
    Berechnet die Pixelbreite eines Strings
    """
    total = 0
    for ch in text:
        width, _ = font.FONT.get(ord(ch), font.FONT[32])
        total += width + spacing
    return total

I guess that’s it, then. Good luck and have fun.
Hans

1 Like

This is excellent Hans! Thank you for posting all of your modifications, code, and font converter. This is very helpful.

A question I have is concerning the main LiPo battery. What is your LiPo battery’s capacity and how long can the unit operate independently from external power?

David

Thank you - I will give this code a go.

Two things to consider:

Hardware:
Putting the TP4046 directly on the LiPo is a risk you should be aware of. It can get really hot and that is not good for the LiPo underneath it.

Software:
I think your solution is elegant, but inefficient. It won’t work with larger fonts and/or more characters, especially not on systems with a limited amount of memory (think RP2040). You save the font as Python code, so it must be compiled and then it is part of the MicroPython memory.

This will work for small characters sets (e.g. numbers), but once you want to put generic strings on your display, say with an additional large heading, you can run into problems.

Have you had a look at BDF fonts? You can convert any TTF to BDF and then you have all the information your converter creates already in that file, in a more binary format. You do have to rewrite your renderer a bit, but not much. The big advantage is that you work with data, not code, so garbage collection will kick in and give you memory back once your done.

..like this approach. @bablokb would you please be so kind as to elaborate on this one and possibly share some code? I like to render any font of my liking on eINK displays, such as Pimoronis badger series and/or inky frames. Thinking about this, it should be even display agnositc


At least, we have 4 different methods discussed here on how to convert fonts for the PICOs and bring them to live on low resource controller chips, but I am always seeking for the most easy and least memory consuming one


As soon as I found “my favorite method” I would create a library for easy use


You can basically use the CircuitPython libraries and replace everything that does the actual “writing” with what you know from PimoroniPython. But the code is 99% version-agnostic, so there is not so much to change. The relevant libraries are:

The first lib does the actual font-loading, i.e. it creates the glyph objects. The second deals with texts (sizes, bounding-box, positioning relative to an anchor point and so on). Note that “texts” can be anything that fits into a valid TTF-font, e.g. it automagically also supports icon fonts.

Well, with PimoroniPython you have a canvas/framebuffer to write on, so it should be display-agnostic as long as you don’t hard-code display sizes.

2 Likes

@David
I use a LiPo with a capacity of 2500 mAh.
For long-term use, I would recommend a 5 volt power supply. I need to measure the power consumption when I get the chance.

@bablokb
I secured the TP4046 using two pieces of adhesive tape that are 1.2 mm thick and 3 mm wide. This leaves an air gap between LiPo and Chip.

Thanks for the suggestion about BDF fonts. I’ll take a look.
The storage overview currently shows the following for the RP2350B:

=== FILE TREE ===
!Test fonts.py (1914 bytes)
MSensor-RTC-Clock-3d.py (8790 bytes)
MSensor-RTC-Clock-4c.py (11083 bytes)
backgroundforscreen.jpg (14096 bytes)
backgroundforscreen.png (112238 bytes)
balls_demo.py (1585 bytes)
button_test.py (2383 bytes)
clock.af (2764 bytes)
clock.py (4683 bytes)
cubes.py (4322 bytes)
double_tap.py (1629 bytes)
image_gallery.py (2801 bytes)
[DIR] lib (474185 bytes)
explorer.py (2997 bytes)
font10.py (25344 bytes)
font12.py (37750 bytes)
font12b.py (37771 bytes)
font16.py (46371 bytes)
font16b.py (50257 bytes)
font20.py (58347 bytes)
font24.py (73722 bytes)
font24b.py (79653 bytes)
font66.py (55534 bytes)
font7.py (5356 bytes)
fontdraw.py (1083 bytes)
main.py (4820 bytes)
maze.py (6604 bytes)
potentiometer.py (1825 bytes)
rainbow.py (1791 bytes)
shake.py (4133 bytes)
single_servo.py (2880 bytes)
step_counter.py (1963 bytes)
tone_song.py (3787 bytes)
walking.png (6341 bytes)
weather_station.py (5407 bytes)

=== FLASH ===
Total: 14680064 bytes
Free : 13881344 bytes

=== RAM ===
Free : 434880 bytes
Used : 5248 bytes

That’s not too bad. It is important to create font files that contain only the characters you actually need.

Thanks, Hans, for sharing the battery details. When you get a chance to check your actual power consumption, it would be great to know what numbers you’re seeing. I looked into some of the Pimoroni displays and Picos quite a while back, and although I don’t remember the exact consumption figures, my impression was that the batteries were really only suitable for brief, intermittent outages. Since I hadn’t added a real‑time clock at the time, any power drop meant the clock lost its time, which made the whole setup a bit inconvenient—especially if I wanted to give one as a gift.

Will this withstand really high temperatures? The junction temperature of the TP4056 is 145°C according to the datasheet. When it reaches this value internally, it will shut down. The external temperature will be lower, but it can still be so hot that you cannot touch it.

But as I wrote, you should be aware of the risk and I see that you are and that you took some measures, so that is ok. Personally, I would not use the TP4046 anymore, mainly because you cannot charge it reliably if the load is connected. In your specific setup it might still work, since the load of the RP2350 and the display won’t cause a significant voltage drop.

Yes, this is very important and that is what I do with BDF too. FontForge allows me to convert a subset of the characters as well. But since the BDF-file is a text-file, I usually convert the whole font and then for specific applications I only cut and paste what I need.

The BDF has a header with font-specific information and a block for every character like this:

STARTCHAR colon
ENCODING 58
SWIDTH 399 0
DWIDTH 21 0
BBX 9 28 6 0
BITMAP
FF80
FF80
FF80
FF80
FF80
FF80
FF80
FF80
FF80
FF80
0000
0000
0000
0000
0000
0000
0000
0000
FF80
FF80
FF80
FF80
FF80
FF80
FF80
FF80
FF80
FF80
ENDCHAR

I did a lot of measurements with the Pico-family. Without peripherals, you can expect something like 25mA@5V when working and about 15mA when sleeping. But the exact figures depend on the electronics and the implementation of sleep, so you have to indeed measure the board. The display on the explorer will typically draw 80mA.

@bablokb

I never charge the battery while the load is connected (see my note in the second picture—unfortunately, it’s in German). But you’re right. I’ll remove the TP4046 and find a better spot for it.

@David
I’ve adjusted the TP4046’s output to 4.8 V. The current draw is then measured at 110 mA. Conservatively estimated, the battery will last about 20 hours.
Another idea:
Set the RTC once and then set the setup wait time to zero. Replace the on/off switch with a pushbutton. Now you can read the time and measured values by pressing the button. When you release it, the Explorer turns off again. This way, the battery lasts a very long time.

Why that? The LDO of the Explorer will bring this down again to 3.3V.

Why that? Because Pimoroni’s schematic specifies that the battery connector expects voltages between 3.5 and 5.5 V. Are there any drawbacks to using 4.8 V? Sorry, I’m a mechanical engineer and just a hobbyist when it comes to electronics. Same goes for programming. But I still love learning new things.

Battery and USB-voltages are fed through Schottky diodes to prevent back charging. These diodes have a voltage drop of about 0.3V. So with 3.5V (lower limit as suggested by Pimoroni) from the battery, the LDO sees 3.2V which is on the low side but will still work.

The drawback of using 4.8V is power dissipation. The formula is roughly (Vin-Vout)xIout. So the LDO will dissipate more power if Vin is higher. So you will drain your battery twice: the “TP4060” has to raise the voltage, and the LDO will lower it again.

Note: the TP4060 is only the charger component. Your breakout seems to have an integrated DC-DC converter at the top. If you have details, e.g. a link, you could look up the efficiency of this DC-DC converter.

The only place where a higher Vbat could be useful is the amplifier for the speaker. The amp is connected directly to Vsys (Vsys = Vbat - 0.3V). You would have to test the volume for lower Vbat.

Bernhard, thank you very much for the excellent explanation. That’s exactly what I expect from a forum.
In that case, it would probably be best to remove the TP4046. I can charge the battery in other ways as well. I’ll see what effect connecting the battery directly has on power consumption.
Hans

1 Like

Thanks guys for the measurements. One of my thoughts to prolong the battery was to have the Pico sense if it was connected to external 5v or if it was running on batteries. If on batteries turn off the display and only show time with a pushbutton as you had mentioned.

My reason for this was that I had modified some example code to run a screensaver on the display. A coworker saw this when I was working on it during my lunch and really liked it. Don’t be too harsh with the crits LOL! Button Y starts the screensaver.

# Clock from: https://forums.pimoroni.com/t/pimoroni-explorer-kit-tutorial/26501/6
# Analog/digital clock example: Tony Goodhew, Leicester UK, Nov 6 2024
# Time adjust example: Tony Goodhew, Leicester UK, Dec 23 2024
# A spinny rainbow wheel example: Pimoroni Explorer 2024 
# All modified, expanded, and combined: David Warner, Detroit MI, May 2025
# Uses the Pimoroni Multi-Sensor Stick and Pimoroni Explorer


""" ---------- potential improvements ------------ """
# Note: Possibly use BME280 and LDR instead of multi-sensor stick
# Note: Backlighting value could be tied to lux value
# Note: A time-of-flight sensor trigger could end screensaver

""" ---------------- Todo List ------------------- """
# Todo: Limit Lux max value so that the clock ring is not overwritten
# Todo: Refine temperature calibration reading via temp_offset
# Todo: delete lux ems test print to console
# Todo: Add a battery back-up or pico-lipo for short term power loss
# Todo: use button_ext to exit clock set


from explorer import display, i2c, button_a, button_b, button_c, button_z, button_x, button_y, YELLOW
from breakout_bme280 import BreakoutBME280
from breakout_ltr559 import BreakoutLTR559
import math, time, sys, qrcode

# get display dimensions in pixels
WIDTH, HEIGHT = display.get_bounds()		# explorer display is 320*240

# backlight variable for cl_set_disp function (range is 0.0 to 1.0)
back_lt = 0.95	

# set pen rgb (red, green, blue) colors
BLACK = display.create_pen(0, 0, 0)
WHITE = display.create_pen(253, 240, 213)
RED = display.create_pen(193, 18, 31)
BLUE = display.create_pen(37, 87, 115)
  
# create lists for days and months 
days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

# create clock center x and Y plus radius
cx, cy, l = 212, 132, 105 	# clock face center x, y, and radius 

# get current time and date: default epoch = (1970, 1, 1, 0, 0, 0, 3, 1) 
year, month, day, h, m, s, wd, _ = time.localtime()	

# Check if sensor stick is connected, if not exit with message 
try:
    bme = BreakoutBME280(i2c, address=0x76)
    ltr = BreakoutLTR559(i2c)
except RuntimeError:
    display.set_layer(0)
    display.set_pen(BLACK)
    display.clear()
    display.set_pen(YELLOW)
    display.text("Multi-Sensor", 60, 95, 320, 3)
    display.text("Stick missing", 60, 125, 320, 3)
    display.update()
    sys.exit()

# initialize exponential moving average (ema) variables
alpha = 0.5  	# lower alpha for smoother response, range is >0 to <=1
lux_ems = 30	# initial lux starting point

# # function to append latest and pop oldest latest_lux
# def append_and_manage_list(lst, new_latest_lux, max_size):
#     lst.append(new_latest_lux)	# append the latest latest_lux to the end
#     if len(lst) > max_size:
#         lst.pop(0)  			# remove the oldest latest_lux 
#     return lst  

# function to run rainbow_wheel
def rainbow_wheel():
    # drawing constants
    INNER_RADIUS = 32
    OUTER_RADIUS = 115
    NUMBER_OF_LINES = 26
    HUE_SHIFT = 0.015
    ROTATION_SPEED = 1.5
    LINE_THICKNESS = 2	# integer
    DIRECTION = -1		# line direction or skew
    
    # variables to keep track of rotation and hue positions
    r = 0
    t = 0
    d = -160	# initial rotation position (degrees)
    
    while True:
        # return to main clock program
        while button_y.value() == 0:
            time.sleep(0.3) 		# delay for pseudo button debounce
            cl_set_disp(0.95)		# clear and set display
            dr_clk_face()			# draw clock face
            return				# return to main clock loop
        
        d += DIRECTION
        if d == -180 or d == 180:  	# change direction at limits
            DIRECTION *= -1			# DIRECTION = DIRECTION * -1
        display.set_pen(BLACK)
        display.clear()
        
        # line and hue drawing routine
        for i in range(0, 360, 360 // NUMBER_OF_LINES):     # (start, end, step size)
            # set pen via hsv(hue, saturation, value) by incrementing hue by t += HUE_SHIFT for every line
            display.set_pen(display.create_pen_hsv((i / 360) + t, 1.0, 1.0))
            # Draw some lines, offset by the rotation variable
            display.line(int(WIDTH / 2 + math.cos(math.radians(i - d/0.7 + r)) * INNER_RADIUS),
                          int(HEIGHT / 2 + math.sin(math.radians(i - d/1.2 + r)) * INNER_RADIUS),
                          int(WIDTH / 2 + math.cos(math.radians(i + d*0.7 + r)) * OUTER_RADIUS),
                          int(HEIGHT / 2 + math.sin(math.radians(i + d/1.5 + r)) * OUTER_RADIUS),
                          LINE_THICKNESS)  
        display.update()
        r += ROTATION_SPEED
        t += HUE_SHIFT

# function to clear layers, set backlighting, and update display 
def cl_set_disp(bl):	# backlight value is passed to function when called
    display.set_backlight(bl)	
    display.set_layer(0)
    display.set_pen(BLACK)
    display.clear()
    display.set_layer(1)
    display.set_pen(BLACK)
    display.clear()
    display.update()

# function to clear layers, but not update display
def clean():    
    display.set_layer(0)
    display.set_pen(BLACK)
    display.clear()
    display.set_layer(1)
    display.set_pen(BLACK)
    display.clear()

# function for hour and minute hands
def hand(ang, long): 
    ang = ang-90
    ls = long/13	# was originally "long/10" for wider hands at pivot
    x0 = int(round(long * math.cos(math.radians(ang))))+cx
    y0 = int(round(long * math.sin(math.radians(ang))))+cy
    x1 = int(round(ls * math.cos(math.radians(ang+90))))+cx	# + x width coordinate perpindicular to hand length
    y1 = int(round(ls * math.sin(math.radians(ang+90))))+cy	# + y width coordinate perpindicular to hand length
    x2 = int(round(ls * math.cos(math.radians(ang-90))))+cx	# - x width coordinate perpindicular to hand length
    y2 = int(round(ls * math.sin(math.radians(ang-90))))+cy	# - y width coordinate perpindicular to hand length
    display.triangle(x0,y0,x1,y1,x2,y2)
    display.circle(cx,cy,int(ls))

# function to set time manually 
def set_time():
    # button assignment: x = ^, y = v, z = >, c = <, a = set
            
    maxx = [12,31,99,24,59]	# maximum values [mo, day, year, "space", hrs, min]
    minn = [1,1,25,1,0]		# minimum values [mo, day, year, "space", hrs, min]
    val = [6,15,25,12,30]	# initial values [mo, day, year, "space", hrs, min]
      
    def show_vals(cur,val):
        for i in range(5):
            display.set_pen(WHITE)
            # display labels: (string, x, y, wordwrap, scale, angle, spacing)
            display.text("Month", 40, yt, 140, 2, 90)
            display.text("Day", 90, yt, 140, 2, 90)
            display.text("Year", 140, yt, 140, 2, 90)
            display.text("Hour 1 - 24", 190, yt, 140, 2, 90)
            display.text("Minute", 240, yt, 140, 2, 90)
            
            xx = 30 + i * 49 	# x-axis position for val display
            display.set_pen(RED)
            if cur == i:
                display.set_pen(YELLOW)
            display.text(str(val[i]),xx,yy,200,2)

    p = 0		# initialize p (pointer) to 0
    yt = 106	# yt is for label display hight on display
    yy = 76		# yy is for val display hight on display
    show_vals(p,val)
    display.update()  
    
    while button_a.value() == 1: # halt loop with "set" button a
        
        # draw icons on screen for buttons
        clean()											# clear layers but do not update display
        display.set_pen(WHITE)
        display.circle(18, 44, 5)   					# "set" dot, display.rectangle(x, y, r)
        display.text("Set", 35, 38, 100, 2)				# "set" text, display.text(text, x, y, wordwrap, scale)
        display.triangle(303, 38, 293, 51, 313, 51)		# "up" arrow, display.triangle(x1, y1, x2, y2, x3, y3)
        display.triangle(303, 127, 293, 114, 313, 114)	# "down" arrow, display.triangle(x1, y1, x2, y2, x3, y3)
        display.triangle(12, 195, 28, 205, 28, 185)		# "previous" arrow, display.triangle(x1, y1, x2, y2, x3, y3)
        display.triangle(311, 195, 295, 205, 295, 185)	# "next" arrow, display.triangle(x1, y1, x2, y2, x3, y3)
        
        if button_x.value() == 0:
            time.sleep(0.2)		# delay for pseudo button debounce
            val[p] = val[p] +1
            if val[p] > maxx[p]:
                val[p] = maxx[p]

        elif button_y.value() == 0:
            time.sleep(0.2)		# delay for pseudo button debounce
            val[p] = val[p] - 1
            if val[p] < minn[p]:
                val[p] = minn[p]
        
        elif button_z.value() == 0:
            time.sleep(0.2)		# delay for pseudo button debounce
            p = p + 1
            if p > 4:
                p = 0
        
        elif button_c.value() == 0:	
            time.sleep(0.2)		# delay for pseudo button debounce
            p = p - 1
            if p < 0:
                p = 4
        
        show_vals(p,val)
        display.update()
        time.sleep(0.1)
        
    cl_set_disp(back_lt)	# clear display and set backlight
    
    mo_set = val[0]
    day_set = val[1]
    yr_set = val[2] + 2000
    h_set = val[3]
    m_set = val[4]
        
    # update real time clock (rtc) with user entered values 
    rtc = machine.RTC()				# create real time clock object
    
    # set rtc time (yr, mo, day, weekday, h, m, s, sub-seconds)
    rtc.datetime((yr_set, mo_set, day_set, 0, h_set, m_set, 0, 0))	
    current_time = rtc.datetime()	# read the updated current time

# function to display "about" screen
def about():
    # clear screen and initialize variables
    hoffset = 50	# horizontal (x) offset
    voffset = 122	# vertical (y) offset
    scale = 3		# scale factor (integer) for micropython logo
    display.set_pen(BLACK)
    display.clear()
    
    # display micropython logo
    # legend:
    # 	display.rectangle(x, y, w, h)
    # 	display.line(x1, y1, x2, y2, thickness)
    display.set_pen(WHITE)
    display.rectangle(0+hoffset, 0+voffset, 32*scale, 32*scale)		
    display.set_pen(BLACK)
    display.rectangle((2*scale)+hoffset, (2*scale)+voffset, 28*scale, 28*scale)	
    display.set_pen(WHITE)
    display.line((9*scale)+hoffset, (8*scale)+voffset, (9*scale)+hoffset, (30*scale)+voffset, scale)
    display.line((16*scale)+hoffset, (2*scale)+voffset, (16*scale)+hoffset, (24*scale)+voffset, scale)	
    display.line((23*scale)+hoffset, (8*scale)+voffset, (23*scale)+hoffset, (30*scale)+voffset, scale)
    display.rectangle((26*scale)+hoffset, (24*scale)+voffset, 2*scale, 4*scale)

    # display credits  
    display.set_font("bitmap6")
    display.text('Powered by', 110, 40, 320, 2) # display.text(x, y, l, size)
    display.text('MicroPython', 105, 65, 320, 2)
    display.text('2025 D. Warner', 98, 90, 320, 2)

    # update display with micropython logo and credits text
    display.update()
    
    # -------------------
    # draw qr code     
    FG = display.create_pen(0, 0, 0)
    BG = display.create_pen(253, 240, 213)

    def measure_qr_code(size, code):
        w, h = code.get_size()
        module_size = int(size / w)
        return module_size * w, module_size

    def draw_qr_code(ox, oy, size, code):
        size, module_size = measure_qr_code(size, code)
        display.set_pen(FG)
        display.rectangle(ox, oy, size, size)
        display.set_pen(BG)
        for x in range(size):
            for y in range(size):
                if code.get_module(x, y):
                    display.rectangle(ox + x * module_size, oy + y * module_size, module_size, module_size)

    code = qrcode.QRCode()
    code.set_text("blend3d@gmail.com")

    display.set_pen(BG)
    max_size = min(WIDTH/2, HEIGHT/2)
    size, module_size = measure_qr_code(max_size, code)
    left = int((WIDTH // 2) - (size // 2)) + 70		# scale 1/2 of display and offset 70 pixels
    top = int((HEIGHT // 2) - (size // 2)) + 50		# scale 1/2 of display and offset 50 pixels
    # call draw_qr_routine
    draw_qr_code(left, top, max_size, code)
    
    # update display with qr code added
    display.update()
    # -------------------
    
    # sleep for a few seconds
    time.sleep(7)
    
    # clear display and set backlight
    cl_set_disp(back_lt)	

# function to draw clock face and titles
def dr_clk_face():
    display.set_pen(BLUE)	# create large blue circle
    display.circle(cx,cy,l)	
    display.update()
    display.set_pen(WHITE)	# create white marks on ring every 6 degrees
    for angle in range(0,360,6):
        xx = int(round(l * math.cos(math.radians(angle))))
        yy = int(round(l * math.sin(math.radians(angle))))    
        display.line(cx,cy,cx+xx,cy+yy)
    display.set_pen(BLACK)	# overlay black circle on blue circle for 8 pixel thick blue ring effect
    display.circle(cx,cy,l-8)	
    display.set_pen(WHITE)	# overlay long white lines every 30 deg to correspond with clock facenumbers
    for angle in range(0,360,30):
        xx = int(round(l * math.cos(math.radians(angle))))
        yy = int(round(l * math.sin(math.radians(angle))))    
        display.line(cx,cy,cx+xx,cy+yy)
    
    display.set_pen(WHITE)	# sensor titles in white
    display.text("Ambient Light",5,80,300,1)
    display.text("Rel Humidity",5,120,300,1)
    display.text("Temperature",5,160,310,1)
    display.text("Air Pressure",5,200,300,1)

# test if year is current, if not call set_time() 
if year <= 2024:	# test if year is less than 2024
    set_time()

# call function to clear display layers, set backlighting, update display
cl_set_disp(back_lt)	# backlight set to 1.0 

#  call function to draw clock face and sensor titles
dr_clk_face()

# ----------------------- main loop ------------------------
while True:
    # draw time set button for manual time set
    display.circle(312, 200, 3)			# "set" dot, display.rectangle(x, y, r)
    display.text("Set",305,210,320,1)	# "set" text
    
    # goto time setting routine upon button 'Z' input
    while button_z.value() == 0:
        time.sleep(0.3) 	# delay for pseudo button debounce
        set_time()			# call set_time function
        dr_clk_face()		# call draw clock face upon return from set time function
    
    # draw about button for info screen
    display.circle(312, 50, 3)			# "about" dot, display.rectangle(x, y, r)
    display.text("About",290,35,320,1)	# "about" text
    
    # go to about screen routine upon button 'x' input
    while button_x.value() == 0:
        time.sleep(0.3) 	# delay for pseudo button debounce
        about()				# call about function
        dr_clk_face()		# call draw clock face upon return from set time funct
        
    # external program call to rainbow_wheel function
    while button_y.value() == 0:
        time.sleep(0.3) 	# delay for pseudo button debounce
        rainbow_wheel()		# call routine for rainbow wheel
    
    # draw face numbers
    display.set_pen(BLACK)
    display.circle(cx,cy,93) 	# clear centre of clock face
    display.set_pen(WHITE)
    display.text("9", 130, 118, 320, 3)
    display.text("3", 280, 118, 320, 3)
    display.text("10", 140, 80, 320, 3)
    display.text("2", 270, 80, 320, 3)
    display.text("1", 242, 55, 320, 3)
    display.text("11", 172, 55, 320, 3)
    display.text("12", 205, 47, 320, 3)
    display.text("6", 205, 196, 320, 3)	
    display.text("7", 168, 186, 320, 3)
    display.text("5", 242, 186, 320, 3)
    display.text("4", 270, 160, 320, 3)
    display.text("8", 142, 160, 320, 3)
    
    # draw hands
    mang = int((m + s/60)* 6)   # angle of minute hand
    hang = int((h + m/60 )* 30)	# angle of hour hand
    display.set_pen(BLUE)
    hand(mang,90)
    display.set_pen(RED)      
    hand(hang,65)
    lens = 93					# length of second hand
    sang = 6 * s - 90			# angle of second hand
    xs = int(round(lens * math.cos(math.radians(sang))))
    ys = int(round(lens * math.sin(math.radians(sang))))
    display.set_pen(WHITE)
    display.line(cx,cy,cx+xs,cy+ys)
    display.circle(cx,cy,3)
    
    # convert 24 hours to 12 hours with am/pm "time of day" (tod) labels
    tod = "AM"			# start with am
    if h >= 12:			# check if hours are greater than 12
        tod = "PM"		# if true time of day is pm
        h -= 12			# if true subtract 12 hours
    if h == 0:			# check for zero, ie noon or midnight
        h = 12			# if true add 12 hours
    
    # assemble digital time
    ms = f"{m:02}"			# f-string formats value to two digits and, 
    hs = f"{h:02}"			# adding a leading zero if needed
    dt = f"{hs}:{ms} {tod}"	# concatenate hr, min, and tod with colons(:) or a space in-between
    
    # clear text areas on display
    display.set_pen(BLACK)
    display.rectangle(5,10,170,18)	# time area
    display.rectangle(5,50,90,18)	# date area
    display.rectangle(5,90,101,18)	# lux area
    display.rectangle(5,130,80,18)	# humidity
    display.rectangle(5,170,105,28)	# temperature
    display.rectangle(5,210,115,18)	# pressure
    
    # write digital time and tod
    display.set_pen(BLUE)
    display.text(dt,5,10,320,3)
 
    # read lux sensors [Prox, ALS_0, ALS_1, Integration_time, Gain, Ratio, Lux] 
    prox, a, b, c, d, e, lux = ltr.get_reading()
    
    # calculate lux exponential moving average (ema)
    latest_lux = int(lux)
    lux_ems = alpha * latest_lux + (1 - alpha) * lux_ems	# calculate ems lux value
    # Todo: delete lux ems test print to console
    print("LUX Sensor: ", latest_lux, "LUX EMS Value:", int(lux_ems))
    
    # read bme280 sensor and convert to us measures
    temperature, pressure, humidity = bme.read()
    temp_f = round(((temperature*1.8)+32),1)			# convert temp to °F, round to 1 place
    pressure_inhg = round((pressure*0.0002952998),2) 	# convert press to inHg, round to 2 places
    
    # display sensor readings
    display.set_pen(RED)
    display.text(str(int(lux_ems))+" lx",5,90,320,3)# display lux, integers only
    display.text(str(int(humidity)) +" %", 5,130,320,3)	# display humidity, integers only
    display.text(str(temp_f)+" °F",5,170,320,3) 		# display temperature
    display.text(str(pressure_inhg)+" in",5,210,320,3)	# display pressure
        
    # update time and date values
    year, month, day, h, m, s, wd, _ = time.localtime()
    display.set_pen(WHITE)
    display.text(days[wd],5,40,320,1)						# wd is weekday (ie., monday, tuesday, ...)
    display.set_pen(BLUE)
    display.text(str(day)+" "+months[month-1],5,50,320,3)	# [month-1] make time month value equal list[0] 
    display.update()
    time.sleep(0.1)