Pimoroni Explorer Kit Tutorial

The following code demonstrates the use of the Pimoroni Explorer Kit as a desktop auto clock/calendar/weather station. I hope someone finds it useful.

Triangles used to be difficult but the latest version of picographics makes them easy to use. I really like the larger screen. It shows off the 320x240 pixels much better. The multi-sensor-stick is really easy to use.

# Pimoroni Explorer Demo
# Analog/ Digital clock - Tony Goodhew, Leicester UK - 6 Nov 2024
# Uses the Multi-Sensor Stick (BME280 + LTR559 + LSM6DS3)
from explorer import display, i2c, button_z, button_x, button_y, BLACK, WHITE, GREEN, RED, BLUE, YELLOW, CYAN
from breakout_bme280 import BreakoutBME280
from breakout_ltr559 import BreakoutLTR559
import math
import time

# Check sensor stick connected
try:
    bme = BreakoutBME280(i2c, address=0x76)
    ltr = BreakoutLTR559(i2c)
except RuntimeError:
    display.set_layer(0)
    display.set_pen(RED)
    display.clear()
    display.set_pen(WHITE)
    display.text("Multi-Sensor Stick missing", 10, 95, 320, 3)
    display.update()

WIDTH, HEIGHT = display.get_bounds()

# Get current time
year, month, day, h, m, s, wd, _ = time.localtime()

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

# Used for minute and hour hands
def hand(ang, long): 
    ang = ang-90
    ls = long/10
    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
    y1 = int(round(ls * math.sin(math.radians(ang+90))))+cy
    x2 = int(round(ls * math.cos(math.radians(ang-90))))+cx
    y2 = int(round(ls * math.sin(math.radians(ang-90))))+cy
    display.triangle(x0,y0,x1,y1,x2,y2)
    display.circle(cx,cy,int(ls))
    
display.set_backlight(1.0)
display.set_layer(0)
display.set_pen(BLACK)
display.clear()
display.set_layer(1)
display.set_pen(BLACK)
display.clear()

cx = 199 # centre of clock
cy = 119
l = 110

# Draw clock face
display.set_pen(BLUE)
display.circle(cx,cy,110)
display.update()

ls = 94
display.set_pen(WHITE)
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)
display.circle(cx,cy,100)
display.set_pen(WHITE)

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)
    
# Sensor titles
display.set_pen(WHITE)
display.text("Temperature",5,40,310,1)
display.text("Lux",5,80,300,1)
display.text("Rel Humidity",5,120,300,1)
display.text("Air Pressure",5,200,300,1)

# === Main Loop ======
while True:
    # Draw face numbers
    display.set_pen(BLACK)
    display.circle(cx,cy,93) # Clear centre of clock face
    display.set_pen(YELLOW)
    display.text("9",118,108,320,3)
    display.text("3",268,108,320,3)
    display.text("10",128,70,320,3)
    display.text("2",258,70,320,3)
    display.text("1",230,45,320,3)
    display.text("11",160,45,320,3)
    display.text("12",190,37,320,3)
    display.text("6",193,186,320,3)
    display.text("7",156,176,320,3)
    display.text("5",230,176,320,3)
    display.text("4",258,150,320,3)
    display.text("8",130,150,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,70)
    sang = 6 * s - 90
    xs = int(round(ls * math.cos(math.radians(sang))))
    ys = int(round(ls * math.sin(math.radians(sang))))
    display.set_pen(WHITE)
    display.line(cx,cy,cx+xs,cy+ys)
    display.circle(cx,cy,3)
    
    # Assemble Digital time
    ss = "0" + str(s)
    lgs =len(ss)
    ss = ss[lgs-2:lgs]
    ms = "0" + str(m)
    lgs = len(ms)
    ms = ms[lgs-2:lgs]
    hs = "0" + str(h)
    lgs = len(hs)
    hs = hs[lgs-2:lgs]
    dt = hs+":"+ms+":"+ss
    
    # Clear sensor text areas
    display.set_pen(BLACK)
    display.rectangle(5,10,127,18)
    display.rectangle(5,50,90,18)
    display.rectangle(5,90,80,18)
    display.rectangle(5,130,80,18)
    display.rectangle(5,210,115,18)
    display.rectangle(5,163,90,28)
    
    # Write digital time & sensor readings 
    display.set_pen(YELLOW)
    display.text(dt,5,10,320,3)
    # read the sensors
    temperature, pressure, humidity = bme.read()
    temp = round(temperature,1)
    display.set_pen(GREEN)
    display.text(str(temp)+" C",5,50,320,3)
    # Convert pressure to hPa
    pressurehpa = int(pressure / 100)
    display.text(str(int(humidity)) +" %", 5,130,320,3)
    display.text(str(pressurehpa)+" hPa",5,210,320,3)
    prox, a, b, c, d, e, lux = ltr.get_reading()
    lux = int(lux)
    display.set_pen(CYAN)
    display.text(str(lux),5,90,320,3)
    
    # Update date/time
    year, month, day, h, m, s, wd, _ = time.localtime()
    display.text(days[wd],5,163,320,1)
    display.text(str(day)+" "+months[month-1],5,173,320,2)
    display.update()
    time.sleep(0.1) 

Video here: https://youtu.be/p8yvwJqr3u4

If you would like a tutorial on using the tilt aspects of the IMU, please leave a comment.

1 Like

Hi Tony, that looks amazing! I don’t have a Pimoroni Explorer Kit, but I tried your clock on a Pico Plus 2, Display Pack 2.0 and Multi-Sensor Stick, with minor initialisation changes, and it works really well.

Curiously, the picographics module provided in the Pico Plus 2 firmware (v0.0.9) doesn’t seem to include the set_layer() method, but it still works fine without those lines.

I would like to offer an alternative way to create the digital time string, using the % format operator:

dt = "%02d:%02d:%02d" % (h, m, s)

This printf-style method betrays my pre-retirement job as a sysadmin, programming mainly in awk(!). Python has newer, more sophisticated string formatting methods, but this old one still works.

Yes, I would be interested in a tutorial on using the inertial measurement unit, thanks.

Thank you for the comments and the formatting hint.

I will have a look at the accelerator values of the imu and see if I can work something out now that I know someone is interested.

I’ve looked on the web but there appears to be very little about using the accelerators in an IMU to easily measure tilt. It did find a couple of formulae which I used as a starting point.
rad_to_deg = 180/math.pi # = 57.29578
roll = math.atan(ay / az) * rad_to_deg
pitch = math.atan((- ax) / math.sqrt(ay * ay + az * az)) * rad_to_deg

I decided to fix the sensor stick above the display screen wit a couble of small blobs of Blu Tack. (You need a longer cable than the short version supplied with the kit - same price but more useful.)

I found that with the IMU in this position the X and Y accelerator readings did not match with the display screen directions. This is easily swapped over in the program.

My first program reads the full tilt in both the X and Y planes and displays them on the screen. Ranges were -90 degrees to + 90 degrees.

# Display tilt readings from the multi-sensor stick on the Explorer screen
from explorer import display, i2c, BLACK, WHITE, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA
from breakout_ltr559 import BreakoutLTR559
from lsm6ds3 import LSM6DS3
from breakout_bme280 import BreakoutBME280
import time
import math

# Clear all layers first
display.set_layer(0)
display.set_pen(BLACK)
display.clear()
display.set_layer(1)
display.set_pen(BLACK)
display.clear()

try:
    ltr = BreakoutLTR559(i2c)
    lsm = LSM6DS3(i2c)
    bme = BreakoutBME280(i2c)
except OSError:
    # Clear the screen
    display.set_pen(RED)
    display.clear()
    display.set_pen(WHITE)
    display.text("Multi-Sensor stick not detected! :(", 10, 95, 320, 3)
    display.update()

rad_to_deg = 180/math.pi
print(rad_to_deg)

while True:

    # Set the layer we're going to be drawing to.
    display.set_layer(0)
    
#    lux, _, _, _, _, _, prox = ltr.get_reading()
    ax, ay, az, gx, gy, gz = lsm.get_readings() # Read the LSM6DS3 imu
    

    if az == 0: az = 0.000000001 # Avoid division by zero!
    
    yt = -math.atan(ax / az) * rad_to_deg;
    xt = -math.atan(ay / az) * rad_to_deg;
    print(int(round(xt,1)),int(round(yt,1)))
    # Clear all layers first
    display.set_layer(0)
    display.set_pen(BLACK)
    display.clear()
    display.set_layer(1)
    display.set_pen(BLACK)
    display.clear()
    display.set_pen(RED)
    display.text("X-Tilt: " + str(int(round(xt,0))), 0, 0, 320, 2)
    display.line(160+int(xt),20,160+int(xt),239)
    display.set_pen(GREEN)
    display.text("Y-Tilt: " + str(int(round(yt,0))), 160, 0, 320, 2)
    display.line(0,135+int(yt),319,135+int(yt))
    
    display.update()
    time.sleep(0.1)

I found it awkward to fully tilt the screen so decided to limit the movement in the creation of a simple game. Here the player has to tilt the board to position the blue circle directly under the target, a yellow circle. Exact positioning of the centres is required.

# Pimoroni Explorer and Sensor Stick Tilt Demo Game
# Tony Goodhew - 22nd November 2024

'''
    Install sensor stick above the display screen with BluTacK or similar
  
    Tilt the Pimoroni Explorer until the blue circle exactly fits under the yellow target circle.
   
'''
from explorer import display, i2c, BLACK, WHITE, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA
from breakout_ltr559 import BreakoutLTR559
from lsm6ds3 import LSM6DS3
from breakout_bme280 import BreakoutBME280
import time
import math
import random

# Clear all layers first
display.set_layer(0)
display.set_pen(BLACK)
display.clear()
display.set_layer(1)
display.set_pen(BLACK)
display.clear()

try:
    ltr = BreakoutLTR559(i2c)
    lsm = LSM6DS3(i2c)
    bme = BreakoutBME280(i2c)
except OSError:
    # Clear the screen
    display.set_pen(RED)
    display.clear()
    display.set_pen(WHITE)
    display.text("Multi-Sensor stick not detected! :(", 10, 95, 320, 3)
    display.update()

#rad_to_deg = 180/math.pi
x_tar = random.randint(75,245)
y_tar = random.randint(50,220)

while True:
    # Set the layer we're going to be drawing to.
    display.set_layer(0)
    
    ax, ay, az, gx, gy, gz = lsm.get_readings() # Read the LSM6DS3 imu
    
    if az == 0: az = 0.000000001
    yt = int(-math.atan(ax / az) * 120)
    if yt < -90: yt = -90
    if yt > 90: yt = 90
    xt = int(-math.atan(ay / az) * 120)
    if xt < -90: xt = -90
    if xt > 90: xt = 90
    
#    print(int(round(xt,1)),int(round(yt,1)))
    # Clear all layers first
    display.set_layer(0)
    display.set_pen(BLACK)
    display.clear()
    display.set_layer(1)
    display.set_pen(BLACK)
    display.clear()
    display.set_pen(RED)
    display.text("X-Tilt: " + str(int(round(xt,0))), 0, 0, 320, 2)
    display.line(160+int(xt),45,160+int(xt),225)
    display.set_pen(GREEN)
    display.text("Y-Tilt: " + str(int(round(yt,0))), 160, 0, 320, 2)
    display.line(71,135+int(yt),250,135+int(yt))
    display.set_pen(BLUE)
    display.line(69,44,251,44) # Draw blue box
    display.line(69,226,251,226)
    display.line(69,44,69,226)
    display.line(251,44,251,226)
    display.circle(160+int(xt),135+int(yt),5) # Draw current position
    display.set_pen(YELLOW)
    display.circle(x_tar, y_tar,5) # Draw target
    display.update()
    
    # Check if circles exactly overlap
    if (x_tar == 160+int(xt)) and (y_tar == 135+int(yt)): # HIT?
        display.set_pen(BLACK)
        display.clear()
        display.set_pen(RED)
        display.text("BANG",50,120,320,7)
        display.update()
        time.sleep(1.5)
        # Get new target position
        x_tar = random.randint(75,245)
        y_tar = random.randint(50,220)
    time.sleep(0.1)

I hope someone finds this useful.