Initial backup of LTP-305G matrix clock setup on matrixpi

Captures everything needed to redeploy the two-display clock (hour on I2C
0x61, minute on I2C 0x63) on a fresh Pi:

- Both systemd units (matrix0x61.service, matrix0x63.service)
- Deployed Pimoroni script tree, including the local %I (12-hour) clock
  customization
- Vendored upstream sources (ltp305-python, breakout-garden) so restore is
  fully offline-capable
- Boot config snippet enabling I2C
- install.sh that wires it all back up idempotently
- Inventory doc cross-referencing every live-system path

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
dissimulo
2026-05-06 01:32:39 -07:00
commit 030172f523
99 changed files with 4445 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
# Weather example
This example turns your Breakout Garden into a mini weather display
combining indoor temperature and pressure data with a weather icon
indicating the current local weather conditions.
## Pre-requisites
This example requires:
- A Pimoroni [Breakout Garden](https://shop.pimoroni.com/products/breakout-garden-hat-i2c-spi)
- A Pimoroni [BME680 Breakout](https://shop.pimoroni.com/products/bme680-breakout)
- A Pimoroni [1.12" OLED Breakout (SPI)](https://shop.pimoroni.com/products/1-12-oled-breakout)
You'll need the requests (`sudo pip install requests`), geocoder (`sudo pip install geocoder`),
and BeautifulSoup4 (`sudo pip install beautifulsoup4`) libraries to query the Dark Sky weather page.
## Installation
Pop the breakouts into your Breakout Garden, and then run the `install.sh`
script in the root of this repository with `sudo ./install.sh` to automagically
install the libraries to run the I2C breakouts.
For this example you'll need to make sure some additional software is installed:
```
sudo apt install python3-lxml python3-pil
sudo pip3 install requests geocoder beautifulsoup4
```
You'll need to clone and install the library for the 1.12" OLED Breakout (SPI)
as follows:
```
git clone https://github.com/pimoroni/sh1106-python
sudo ./install.sh
```
This example assumes that you have the OLED plugged into the front slot on the
Breakout Garden HAT, which should also work with the Breakout Garden Mini HAT.
To change it to the back slot, change `device=1` to `device=0` on the line
where the OLED is set up.
## Running this example
To run this example, type `./weather.py` in the terminal
## Notes
This example uses Sheffield as the default location, so you'll need to specify your city and
country code at the top of the file, changing the variables called `CITY` and `COUNTRYCODE`
to your current location.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import time
import datetime
import glob
import logging
import sys
try:
import requests
import geocoder
import lxml
from bs4 import BeautifulSoup
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
except ImportError:
print("""
This script requires several modules to run correctly.
Install with:
sudo pip install requests geocoder beautifulsoup4
sudo apt install python{v}-lxml python{v}-pil
""".format(v="" if sys.version_info.major == 2 else sys.version_info.major))
sys.exit(1)
import bme680
from luma.core.interface.serial import spi
from luma.core.error import DeviceNotFoundError
from luma.oled.device import sh1106
TEMPERATURE_UPDATE_INTERVAL = 0.1 # in seconds
# Default to Sheffield-on-Sea for location
CITY = "Sheffield"
COUNTRYCODE = "GB"
# Used to calibrate the sensor
TEMP_OFFSET = 0.0
logging.basicConfig(level=os.environ.get("LOGLEVEL", "WARNING"))
print("""This Pimoroni Breakout Garden example requires a
BME680 Environmental Sensor Breakout and a 1.12" OLED Breakout.
This example turns your Breakout Garden into a mini weather display
combining indoor temperature and pressure data with a weather icon
indicating the current local weather conditions.
Press Ctrl+C a couple times to exit.
""")
# Convert a city name and country code to latitude and longitude
def get_coords(address):
g = geocoder.arcgis(address)
coords = g.latlng
logging.info("Location coordinates: %s", coords)
return coords
# Query Dark Sky (https://darksky.net/) to scrape current weather data
def get_weather(coords):
weather = {}
try:
res = requests.get("https://darksky.net/forecast/{}/uk212/en".format(","
.join([str(c) for c in coords])))
if res.status_code == 200:
soup = BeautifulSoup(res.content, "lxml")
curr = soup.find("span", "currently")
if curr:
img_name = curr.img["alt"].split()[0]
logging.info("Weather summary: %s", img_name)
weather["summary"] = img_name
except requests.exceptions.RequestException as e:
logging.error("Could not get weather data from DarkSky: {}".format(e))
pass
return weather
# This maps the weather summary from Dark Sky
# to the appropriate weather icons
icon_map = {
"snow": ["snow", "sleet"],
"rain": ["rain"],
"cloud": ["fog", "cloudy", "partly-cloudy-day", "partly-cloudy-night"],
"sun": ["clear-day", "clear-night"],
"storm": [],
"wind": ["wind"]
}
# Pre-load icons into a dictionary with PIL
icons = {}
for icon in glob.glob("icons/*.png"):
icon_name = icon.split("/")[1].replace(".png", "")
icon_image = Image.open(icon)
icons[icon_name] = icon_image
location_string = "{city}, {countrycode}".format(city=CITY,
countrycode=COUNTRYCODE)
coords = get_coords(location_string)
def get_weather_icon(weather):
if weather:
summary = weather["summary"]
for icon in icon_map:
if summary in icon_map[icon]:
logging.info("Weather icon: %s", icon)
return icons[icon]
logging.error("Could not determine icon for weather")
return None
else:
logging.error("No weather information provided to get icon")
return None
# Get initial weather data for the given location
weather_icon = get_weather_icon(get_weather(coords))
# Set up OLED
oled = sh1106(spi(port=0, device=1, gpio_DC=9), rotate=2, height=128, width=128)
# Set up BME680 sensor
sensor = bme680.BME680()
sensor.set_humidity_oversample(bme680.OS_2X)
sensor.set_pressure_oversample(bme680.OS_4X)
sensor.set_temperature_oversample(bme680.OS_8X)
sensor.set_filter(bme680.FILTER_SIZE_3)
sensor.set_temp_offset(TEMP_OFFSET)
# Load fonts
rr_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'fonts',
'Roboto-Regular.ttf'))
rb_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'fonts',
'Roboto-Black.ttf'))
rr_24 = ImageFont.truetype(rr_path, 24)
rb_20 = ImageFont.truetype(rb_path, 20)
rr_12 = ImageFont.truetype(rr_path, 12)
# Fetch sensor dating first so that device settings take effect
sensor.get_sensor_data()
# Initial values
low_temp = sensor.data.temperature
high_temp = sensor.data.temperature
curr_date = datetime.date.today().day
last_checked = time.time()
# Main loop
while True:
# Limit calls to Dark Sky to 1 per minute
if time.time() - last_checked > 60:
weather_icon = get_weather_icon(get_weather(coords))
last_checked = time.time()
# Load in the background image
background = Image.open("images/weather.png").convert(oled.mode)
# Place the weather icon and draw the background
if weather_icon:
background.paste(weather_icon, (10, 46))
draw = ImageDraw.ImageDraw(background)
# Gets temp. and press. and keeps track of daily min and max temp
if sensor.get_sensor_data():
temp = sensor.data.temperature
press = sensor.data.pressure
if datetime.datetime.today().day == curr_date:
if temp < low_temp:
low_temp = temp
elif temp > high_temp:
high_temp = temp
else:
curr_date = datetime.datetime.today().day
low_temp = temp
high_temp = temp
# Write temp. and press. to image
draw.text((8, 22), "{0:4.0f}".format(press),
fill="white", font=rb_20)
draw.text((86, 12), u"{0:2.0f}°".format(temp),
fill="white", font=rb_20)
# Write min and max temp. to image
draw.text((80, 0), u"max: {0:2.0f}°".format(high_temp),
fill="white", font=rr_12)
draw.text((80, 110), u"min: {0:2.0f}°".format(low_temp),
fill="white", font=rr_12)
# Write the 24h time and blink the separator every second
if int(time.time()) % 2 == 0:
draw.text((4, 98), datetime.datetime.now().strftime("%H:%M"),
fill="white", font=rr_24)
else:
draw.text((4, 98), datetime.datetime.now().strftime("%H %M"),
fill="white", font=rr_24)
# These lines display the temp. on the thermometer image
draw.rectangle([(97, 43), (100, 86)], fill="black")
temp_offset = 86 - ((86 - 43) * ((temp - 20) / (32 - 20)))
draw.rectangle([(97, temp_offset), (100, 86)], fill="white")
# Display the completed image on the OLED
oled.display(background)
time.sleep(TEMPERATURE_UPDATE_INTERVAL)