Add initial weather display application with API integration
Includes weather data retrieval from Open-Meteo API and basic WiFi setup for connectivity. IntelliJ project configuration files and a `.gitignore` for IDE-specific files are also added.
This commit is contained in:
parent
ddd867eb00
commit
92defbe958
201
BasicFont.py
Normal file
201
BasicFont.py
Normal file
@ -0,0 +1,201 @@
|
||||
# Font bitmaps, more slender than the font built into famebuf
|
||||
# Source https://github.com/KahKitZheng/greenhouse/blob/master/raspberry_pi/grove.py/grove/display/sh1107g.py
|
||||
BasicFont = [
|
||||
[0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x00,0x5F,0x00,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x00,0x07,0x00,0x07,0x00,0x00,0x00],
|
||||
[0x00,0x14,0x7F,0x14,0x7F,0x14,0x00,0x00],
|
||||
[0x00,0x24,0x2A,0x7F,0x2A,0x12,0x00,0x00],
|
||||
[0x00,0x23,0x13,0x08,0x64,0x62,0x00,0x00],
|
||||
[0x00,0x36,0x49,0x55,0x22,0x50,0x00,0x00],
|
||||
[0x00,0x00,0x05,0x03,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x1C,0x22,0x41,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x41,0x22,0x1C,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x08,0x2A,0x1C,0x2A,0x08,0x00,0x00],
|
||||
[0x00,0x08,0x08,0x3E,0x08,0x08,0x00,0x00],
|
||||
[0x00,0xA0,0x60,0x00,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x08,0x08,0x08,0x08,0x08,0x00,0x00],
|
||||
[0x00,0x60,0x60,0x00,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x20,0x10,0x08,0x04,0x02,0x00,0x00],
|
||||
[0x00,0x3E,0x51,0x49,0x45,0x3E,0x00,0x00],
|
||||
[0x00,0x00,0x42,0x7F,0x40,0x00,0x00,0x00],
|
||||
[0x00,0x62,0x51,0x49,0x49,0x46,0x00,0x00],
|
||||
[0x00,0x22,0x41,0x49,0x49,0x36,0x00,0x00],
|
||||
[0x00,0x18,0x14,0x12,0x7F,0x10,0x00,0x00],
|
||||
[0x00,0x27,0x45,0x45,0x45,0x39,0x00,0x00],
|
||||
[0x00,0x3C,0x4A,0x49,0x49,0x30,0x00,0x00],
|
||||
[0x00,0x01,0x71,0x09,0x05,0x03,0x00,0x00],
|
||||
[0x00,0x36,0x49,0x49,0x49,0x36,0x00,0x00],
|
||||
[0x00,0x06,0x49,0x49,0x29,0x1E,0x00,0x00],
|
||||
[0x00,0x00,0x36,0x36,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x00,0xAC,0x6C,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x08,0x14,0x22,0x41,0x00,0x00,0x00],
|
||||
[0x00,0x14,0x14,0x14,0x14,0x14,0x00,0x00],
|
||||
[0x00,0x41,0x22,0x14,0x08,0x00,0x00,0x00],
|
||||
[0x00,0x02,0x01,0x51,0x09,0x06,0x00,0x00],
|
||||
[0x00,0x32,0x49,0x79,0x41,0x3E,0x00,0x00],
|
||||
[0x00,0x7E,0x09,0x09,0x09,0x7E,0x00,0x00],
|
||||
[0x00,0x7F,0x49,0x49,0x49,0x36,0x00,0x00],
|
||||
[0x00,0x3E,0x41,0x41,0x41,0x22,0x00,0x00],
|
||||
[0x00,0x7F,0x41,0x41,0x22,0x1C,0x00,0x00],
|
||||
[0x00,0x7F,0x49,0x49,0x49,0x41,0x00,0x00],
|
||||
[0x00,0x7F,0x09,0x09,0x09,0x01,0x00,0x00],
|
||||
[0x00,0x3E,0x41,0x41,0x51,0x72,0x00,0x00],
|
||||
[0x00,0x7F,0x08,0x08,0x08,0x7F,0x00,0x00],
|
||||
[0x00,0x41,0x7F,0x41,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x20,0x40,0x41,0x3F,0x01,0x00,0x00],
|
||||
[0x00,0x7F,0x08,0x14,0x22,0x41,0x00,0x00],
|
||||
[0x00,0x7F,0x40,0x40,0x40,0x40,0x00,0x00],
|
||||
[0x00,0x7F,0x02,0x0C,0x02,0x7F,0x00,0x00],
|
||||
[0x00,0x7F,0x04,0x08,0x10,0x7F,0x00,0x00],
|
||||
[0x00,0x3E,0x41,0x41,0x41,0x3E,0x00,0x00],
|
||||
[0x00,0x7F,0x09,0x09,0x09,0x06,0x00,0x00],
|
||||
[0x00,0x3E,0x41,0x51,0x21,0x5E,0x00,0x00],
|
||||
[0x00,0x7F,0x09,0x19,0x29,0x46,0x00,0x00],
|
||||
[0x00,0x26,0x49,0x49,0x49,0x32,0x00,0x00],
|
||||
[0x00,0x01,0x01,0x7F,0x01,0x01,0x00,0x00],
|
||||
[0x00,0x3F,0x40,0x40,0x40,0x3F,0x00,0x00],
|
||||
[0x00,0x1F,0x20,0x40,0x20,0x1F,0x00,0x00],
|
||||
[0x00,0x3F,0x40,0x38,0x40,0x3F,0x00,0x00],
|
||||
[0x00,0x63,0x14,0x08,0x14,0x63,0x00,0x00],
|
||||
[0x00,0x03,0x04,0x78,0x04,0x03,0x00,0x00],
|
||||
[0x00,0x61,0x51,0x49,0x45,0x43,0x00,0x00],
|
||||
[0x00,0x7F,0x41,0x41,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x02,0x04,0x08,0x10,0x20,0x00,0x00],
|
||||
[0x00,0x41,0x41,0x7F,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x04,0x02,0x01,0x02,0x04,0x00,0x00],
|
||||
[0x00,0x80,0x80,0x80,0x80,0x80,0x00,0x00],
|
||||
[0x00,0x01,0x02,0x04,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x20,0x54,0x54,0x54,0x78,0x00,0x00],
|
||||
[0x00,0x7F,0x48,0x44,0x44,0x38,0x00,0x00],
|
||||
[0x00,0x38,0x44,0x44,0x28,0x00,0x00,0x00],
|
||||
[0x00,0x38,0x44,0x44,0x48,0x7F,0x00,0x00],
|
||||
[0x00,0x38,0x54,0x54,0x54,0x18,0x00,0x00],
|
||||
[0x00,0x08,0x7E,0x09,0x02,0x00,0x00,0x00],
|
||||
[0x00,0x18,0xA4,0xA4,0xA4,0x7C,0x00,0x00],
|
||||
[0x00,0x7F,0x08,0x04,0x04,0x78,0x00,0x00],
|
||||
[0x00,0x00,0x7D,0x00,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x80,0x84,0x7D,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x7F,0x10,0x28,0x44,0x00,0x00,0x00],
|
||||
[0x00,0x41,0x7F,0x40,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x7C,0x04,0x18,0x04,0x78,0x00,0x00],
|
||||
[0x00,0x7C,0x08,0x04,0x7C,0x00,0x00,0x00],
|
||||
[0x00,0x38,0x44,0x44,0x38,0x00,0x00,0x00],
|
||||
[0x00,0xFC,0x24,0x24,0x18,0x00,0x00,0x00],
|
||||
[0x00,0x18,0x24,0x24,0xFC,0x00,0x00,0x00],
|
||||
[0x00,0x00,0x7C,0x08,0x04,0x00,0x00,0x00],
|
||||
[0x00,0x48,0x54,0x54,0x24,0x00,0x00,0x00],
|
||||
[0x00,0x04,0x7F,0x44,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x3C,0x40,0x40,0x7C,0x00,0x00,0x00],
|
||||
[0x00,0x1C,0x20,0x40,0x20,0x1C,0x00,0x00],
|
||||
[0x00,0x3C,0x40,0x30,0x40,0x3C,0x00,0x00],
|
||||
[0x00,0x44,0x28,0x10,0x28,0x44,0x00,0x00],
|
||||
[0x00,0x1C,0xA0,0xA0,0x7C,0x00,0x00,0x00],
|
||||
[0x00,0x44,0x64,0x54,0x4C,0x44,0x00,0x00],
|
||||
[0x00,0x08,0x36,0x41,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x00,0x7F,0x00,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x41,0x36,0x08,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x02,0x01,0x01,0x02,0x01,0x00,0x00],
|
||||
[0x00,0x02,0x05,0x05,0x02,0x00,0x00,0x00]
|
||||
]
|
||||
|
||||
# Basic font(as above) with the last vertical line of empty data removed
|
||||
# The condensed version of the font is 7 pixels wide instead of 8 (8x7)
|
||||
BasicFontCondensed = [
|
||||
[0x00,0x00,0x00,0x00,0x00],
|
||||
[0x00,0x5F,0x00,0x00,0x00],
|
||||
[0x00,0x07,0x00,0x07,0x00],
|
||||
[0x00,0x14,0x7F,0x14,0x7F,0x14,0x00],
|
||||
[0x00,0x24,0x2A,0x7F,0x2A,0x12,0x00],
|
||||
[0x00,0x23,0x13,0x08,0x64,0x62,0x00],
|
||||
[0x00,0x36,0x49,0x55,0x22,0x50,0x00],
|
||||
[0x00,0x05,0x03,0x00],
|
||||
[0x00,0x1C,0x22,0x41,0x00],
|
||||
[0x00,0x41,0x22,0x1C,0x00],
|
||||
[0x00,0x08,0x2A,0x1C,0x2A,0x08,0x00],
|
||||
[0x00,0x08,0x08,0x3E,0x08,0x08,0x00],
|
||||
[0x00,0xA0,0x60,0x00],
|
||||
[0x00,0x08,0x08,0x08,0x08,0x08,0x00],
|
||||
[0x00,0x60,0x60,0x00],
|
||||
[0x00,0x20,0x10,0x08,0x04,0x02,0x00],
|
||||
[0x00,0x3E,0x51,0x49,0x45,0x3E,0x00],
|
||||
[0x00,0x00,0x42,0x7F,0x40,0x00,0x00],
|
||||
[0x00,0x62,0x51,0x49,0x49,0x46,0x00],
|
||||
[0x00,0x22,0x41,0x49,0x49,0x36,0x00],
|
||||
[0x00,0x18,0x14,0x12,0x7F,0x10,0x00],
|
||||
[0x00,0x27,0x45,0x45,0x45,0x39,0x00],
|
||||
[0x00,0x3C,0x4A,0x49,0x49,0x30,0x00],
|
||||
[0x00,0x01,0x71,0x09,0x05,0x03,0x00],
|
||||
[0x00,0x36,0x49,0x49,0x49,0x36,0x00],
|
||||
[0x00,0x06,0x49,0x49,0x29,0x1E,0x00],
|
||||
[0x00,0x36,0x36,0x00],
|
||||
[0x00,0xAC,0x6C,0x00],
|
||||
[0x00,0x08,0x14,0x22,0x41,0x00],
|
||||
[0x00,0x14,0x14,0x14,0x14,0x14,0x00],
|
||||
[0x00,0x41,0x22,0x14,0x08,0x00],
|
||||
[0x00,0x02,0x01,0x51,0x09,0x06,0x00],
|
||||
[0x00,0x32,0x49,0x79,0x41,0x3E,0x00],
|
||||
[0x00,0x7E,0x09,0x09,0x09,0x7E,0x00],
|
||||
[0x00,0x7F,0x49,0x49,0x49,0x36,0x00],
|
||||
[0x00,0x3E,0x41,0x41,0x41,0x22,0x00],
|
||||
[0x00,0x7F,0x41,0x41,0x22,0x1C,0x00],
|
||||
[0x00,0x7F,0x49,0x49,0x49,0x41,0x00],
|
||||
[0x00,0x7F,0x09,0x09,0x09,0x01,0x00],
|
||||
[0x00,0x3E,0x41,0x41,0x51,0x72,0x00],
|
||||
[0x00,0x7F,0x08,0x08,0x08,0x7F,0x00],
|
||||
[0x00,0x41,0x7F,0x41,0x00],
|
||||
[0x00,0x20,0x40,0x41,0x3F,0x01,0x00],
|
||||
[0x00,0x7F,0x08,0x14,0x22,0x41,0x00],
|
||||
[0x00,0x7F,0x40,0x40,0x40,0x40,0x00],
|
||||
[0x00,0x7F,0x02,0x0C,0x02,0x7F,0x00],
|
||||
[0x00,0x7F,0x04,0x08,0x10,0x7F,0x00],
|
||||
[0x00,0x3E,0x41,0x41,0x41,0x3E,0x00],
|
||||
[0x00,0x7F,0x09,0x09,0x09,0x06,0x00],
|
||||
[0x00,0x3E,0x41,0x51,0x21,0x5E,0x00],
|
||||
[0x00,0x7F,0x09,0x19,0x29,0x46,0x00],
|
||||
[0x00,0x26,0x49,0x49,0x49,0x32,0x00],
|
||||
[0x00,0x01,0x01,0x7F,0x01,0x01,0x00],
|
||||
[0x00,0x3F,0x40,0x40,0x40,0x3F,0x00],
|
||||
[0x00,0x1F,0x20,0x40,0x20,0x1F,0x00],
|
||||
[0x00,0x3F,0x40,0x38,0x40,0x3F,0x00],
|
||||
[0x00,0x63,0x14,0x08,0x14,0x63,0x00],
|
||||
[0x00,0x03,0x04,0x78,0x04,0x03,0x00],
|
||||
[0x00,0x61,0x51,0x49,0x45,0x43,0x00],
|
||||
[0x00,0x7F,0x41,0x41,0x00],
|
||||
[0x00,0x02,0x04,0x08,0x10,0x20,0x00],
|
||||
[0x00,0x41,0x41,0x7F,0x00],
|
||||
[0x00,0x04,0x02,0x01,0x02,0x04,0x00],
|
||||
[0x00,0x80,0x80,0x80,0x80,0x80,0x00],
|
||||
[0x00,0x01,0x02,0x04,0x00],
|
||||
[0x00,0x20,0x54,0x54,0x54,0x78,0x00],
|
||||
[0x00,0x7F,0x48,0x44,0x44,0x38,0x00],
|
||||
[0x00,0x38,0x44,0x44,0x28,0x00],
|
||||
[0x00,0x38,0x44,0x44,0x48,0x7F,0x00],
|
||||
[0x00,0x38,0x54,0x54,0x54,0x18,0x00],
|
||||
[0x00,0x08,0x7E,0x09,0x02,0x00],
|
||||
[0x00,0x18,0xA4,0xA4,0xA4,0x7C,0x00],
|
||||
[0x00,0x7F,0x08,0x04,0x04,0x78,0x00],
|
||||
[0x00,0x7D,0x00],
|
||||
[0x00,0x80,0x84,0x7D,0x00],
|
||||
[0x00,0x7F,0x10,0x28,0x44,0x00],
|
||||
[0x00,0x41,0x7F,0x40,0x00],
|
||||
[0x00,0x7C,0x04,0x18,0x04,0x78,0x00],
|
||||
[0x00,0x7C,0x08,0x04,0x7C,0x00],
|
||||
[0x00,0x38,0x44,0x44,0x38,0x00],
|
||||
[0x00,0xFC,0x24,0x24,0x18,0x00],
|
||||
[0x00,0x18,0x24,0x24,0xFC,0x00],
|
||||
[0x00,0x7C,0x08,0x04,0x00],
|
||||
[0x00,0x48,0x54,0x54,0x24,0x00],
|
||||
[0x00,0x04,0x7F,0x44,0x00],
|
||||
[0x00,0x3C,0x40,0x40,0x7C,0x00],
|
||||
[0x00,0x1C,0x20,0x40,0x20,0x1C,0x00],
|
||||
[0x00,0x3C,0x40,0x30,0x40,0x3C,0x00],
|
||||
[0x00,0x44,0x28,0x10,0x28,0x44,0x00],
|
||||
[0x00,0x1C,0xA0,0xA0,0x7C,0x00],
|
||||
[0x00,0x44,0x64,0x54,0x4C,0x44,0x00],
|
||||
[0x00,0x08,0x36,0x41,0x00],
|
||||
[0x00,0x7F,0x00],
|
||||
[0x00,0x41,0x36,0x08,0x00],
|
||||
[0x00,0x02,0x01,0x01,0x02,0x01,0x00],
|
||||
[0x00,0x02,0x05,0x05,0x02,0x00]
|
||||
]
|
||||
434
PicoOled13.py
Normal file
434
PicoOled13.py
Normal file
@ -0,0 +1,434 @@
|
||||
from machine import Pin, SPI
|
||||
import framebuf
|
||||
import time
|
||||
import BasicFont
|
||||
|
||||
# Pin Definitions
|
||||
DC = 8
|
||||
RST = 12
|
||||
MOSI = 11
|
||||
SCK = 10
|
||||
CS = 9
|
||||
|
||||
KEY0 = 15
|
||||
KEY1 = 17
|
||||
|
||||
# Display object
|
||||
display = None
|
||||
|
||||
|
||||
class OLED_1inch3_SPI(framebuf.FrameBuffer):
|
||||
def __init__(self):
|
||||
self.is_on = 0
|
||||
self.width = 128
|
||||
self.height = 64
|
||||
|
||||
self.white = 0xffff
|
||||
self.black = 0x0000
|
||||
|
||||
self.font = BasicFont.BasicFontCondensed
|
||||
|
||||
# framebuf init
|
||||
self.buffer = bytearray(self.height * self.width // 8)
|
||||
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_HMSB)
|
||||
|
||||
# SPI init
|
||||
self.cs = Pin(CS, Pin.OUT)
|
||||
self.rst = Pin(RST, Pin.OUT)
|
||||
|
||||
self.cs(1)
|
||||
self.spi = SPI(1, 20000_000, polarity=0, phase=0, sck=Pin(SCK), mosi=Pin(MOSI), miso=None)
|
||||
self.dc = Pin(DC, Pin.OUT)
|
||||
self.dc(1)
|
||||
|
||||
# display init
|
||||
self.init_display()
|
||||
|
||||
# Clear the screen on init - needs framebuf init, spi init and display init
|
||||
self.clear()
|
||||
|
||||
# Init Keys
|
||||
self.KEY0 = KEY0
|
||||
self.KEY1 = KEY1
|
||||
self.key0 = Pin(KEY0, Pin.IN, Pin.PULL_UP)
|
||||
self.key1 = Pin(KEY1, Pin.IN, Pin.PULL_UP)
|
||||
|
||||
def is_pressed(self, key):
|
||||
if key == self.KEY0:
|
||||
return not self.key0.value()
|
||||
elif key == self.KEY1:
|
||||
return not self.key1.value()
|
||||
else:
|
||||
return None
|
||||
|
||||
def on(self):
|
||||
if not self.is_on:
|
||||
self.write_cmd(0xAF)
|
||||
self.is_on = 1
|
||||
|
||||
def off(self):
|
||||
if self.is_on:
|
||||
self.write_cmd(0xAE)
|
||||
self.is_on = 0
|
||||
|
||||
def get_width(self):
|
||||
return self.width
|
||||
|
||||
def get_height(self):
|
||||
return self.height
|
||||
|
||||
def write_cmd(self, cmd):
|
||||
self.cs(1)
|
||||
self.dc(0)
|
||||
self.cs(0)
|
||||
self.spi.write(bytearray([cmd]))
|
||||
self.cs(1)
|
||||
|
||||
def write_data(self, buf):
|
||||
self.cs(1)
|
||||
self.dc(1)
|
||||
self.cs(0)
|
||||
self.spi.write(bytearray([buf]))
|
||||
self.cs(1)
|
||||
|
||||
def init_display(self):
|
||||
"""Initialize display"""
|
||||
self.rst(1)
|
||||
time.sleep(0.001)
|
||||
self.rst(0)
|
||||
time.sleep(0.01)
|
||||
self.rst(1)
|
||||
|
||||
self.off()
|
||||
|
||||
self.write_cmd(0x00) # set lower column address
|
||||
self.write_cmd(0x10) # set higher column address
|
||||
|
||||
self.write_cmd(0xB0) # set page address
|
||||
|
||||
self.write_cmd(0xdc) # set display start line
|
||||
self.write_cmd(0x00) # (2nd param)
|
||||
|
||||
self.write_cmd(0x81) # contract control
|
||||
self.write_cmd(0x80) # 128
|
||||
|
||||
self.write_cmd(0x21) # Set Memory addressing mode (0x20/0x21)
|
||||
self.write_cmd(0xa0) # set segment remap
|
||||
self.write_cmd(0xc0) # Com scan direction
|
||||
self.write_cmd(0xa4) # Disable Entire Display On (0xA4/0xA5)
|
||||
self.write_cmd(0xa6) # normal / reverse
|
||||
|
||||
self.write_cmd(0xa8) # multiplex ratio ??
|
||||
self.write_cmd(0x3f) # duty = 1/64 ??
|
||||
|
||||
self.write_cmd(0xd3) # set display offset
|
||||
self.write_cmd(0x60)
|
||||
|
||||
self.write_cmd(0xd5) # set osc division
|
||||
self.write_cmd(0x50)
|
||||
|
||||
self.write_cmd(0xd9) # set pre-charge period
|
||||
self.write_cmd(0x22)
|
||||
|
||||
self.write_cmd(0xdb) # set vcomh
|
||||
self.write_cmd(0x35)
|
||||
|
||||
self.write_cmd(0xad) # set charge pump enable
|
||||
self.write_cmd(0x8a) # Set DC-DC enable (a=0:disable; a=1:enable)
|
||||
|
||||
self.on()
|
||||
|
||||
# --- Helper functions for scaling bitmaps ---
|
||||
def _unpack_bitmap(self, data, width, height):
|
||||
"""
|
||||
Unpack a MONO_VLSB bitmap into a 2D list of 0s and 1s.
|
||||
Each byte in data represents one vertical column of 8 pixels.
|
||||
"""
|
||||
arr = [[0] * width for _ in range(height)]
|
||||
for x in range(width):
|
||||
b = data[x]
|
||||
for y in range(height):
|
||||
arr[y][x] = 1 if (b >> y) & 1 else 0
|
||||
return arr
|
||||
|
||||
def _scale_2d_array(self, arr, scale):
|
||||
"""
|
||||
Scale a 2D list (of 0s and 1s) by an integer factor using pixel replication.
|
||||
"""
|
||||
h = len(arr)
|
||||
w = len(arr[0])
|
||||
new_arr = [[0] * (w * scale) for _ in range(h * scale)]
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
pixel = arr[y][x]
|
||||
for dy in range(scale):
|
||||
for dx in range(scale):
|
||||
new_arr[y * scale + dy][x * scale + dx] = pixel
|
||||
return new_arr
|
||||
|
||||
def _pack_bitmap(self, arr):
|
||||
"""
|
||||
Pack a 2D list of 0s and 1s into a MONO_VLSB bytearray.
|
||||
The height must be a multiple of 8.
|
||||
"""
|
||||
height = len(arr)
|
||||
width = len(arr[0])
|
||||
buf = bytearray(width * (height // 8))
|
||||
for x in range(width):
|
||||
for byte_row in range(height // 8):
|
||||
b = 0
|
||||
for bit in range(8):
|
||||
y = byte_row * 8 + bit
|
||||
if arr[y][x]:
|
||||
b |= (1 << bit)
|
||||
buf[x + byte_row * width] = b
|
||||
return buf
|
||||
|
||||
def _scale_bitmap(self, data, width, height, scale):
|
||||
"""
|
||||
Scale a MONO_VLSB bitmap (provided as a bytearray) by an integer factor.
|
||||
Returns the scaled bytearray and the new dimensions.
|
||||
"""
|
||||
arr = self._unpack_bitmap(data, width, height)
|
||||
scaled_arr = self._scale_2d_array(arr, scale)
|
||||
new_buffer = self._pack_bitmap(scaled_arr)
|
||||
new_width = width * scale
|
||||
new_height = height * scale
|
||||
return new_buffer, new_width, new_height
|
||||
|
||||
# --- Modified text() method with scaling support ---
|
||||
# The original text is assumed to be 8 pixels high.
|
||||
def text(self, s, x0, y0, col=0xffff, wrap=1, just=0, scale=1):
|
||||
"""
|
||||
Draw text on the display starting at (x0, y0).
|
||||
|
||||
Parameters:
|
||||
s : The string to render.
|
||||
x0, y0 : Starting coordinates.
|
||||
col : Color (default white). If 0, the text is inverted.
|
||||
wrap : Text wrapping mode.
|
||||
0: Clip at the right edge.
|
||||
1: Wrap to the next line.
|
||||
Other: Wrap to the original x0 coordinate.
|
||||
just : Justification.
|
||||
0: Left-justified.
|
||||
1: Right-justified.
|
||||
2: Center-justified.
|
||||
scale : Scaling factor for enlarging the font (default is 1, i.e. no scaling).
|
||||
|
||||
Returns:
|
||||
A list [new_x, new_y] with updated coordinates after rendering the text.
|
||||
"""
|
||||
if len(s) == 0:
|
||||
return (x0, y0)
|
||||
|
||||
x = x0
|
||||
pixels = bytearray([])
|
||||
# For each character in the string, accumulate its bitmap data
|
||||
for i in range(len(s)):
|
||||
C = ord(s[i])
|
||||
if C < 32 or C > 127:
|
||||
C = 32
|
||||
cdata = self.font[C - 32]
|
||||
effective_char_width = len(cdata) * scale
|
||||
effective_pixels_width = len(pixels) * scale
|
||||
# Check if adding the next character would exceed the display width
|
||||
if len(pixels) and (
|
||||
(just == 0 and x + effective_pixels_width + effective_char_width > self.width) or
|
||||
(just == 1 and x - effective_pixels_width - effective_char_width < 0) or
|
||||
(just == 2 and (x - effective_pixels_width / 2 - effective_char_width < 0 or
|
||||
x + effective_pixels_width / 2 + effective_char_width > self.width))
|
||||
):
|
||||
# Invert pixels if needed
|
||||
if col == 0:
|
||||
for j, v in enumerate(pixels):
|
||||
pixels[j] = 0xFF & ~v
|
||||
# Create a frame buffer from the accumulated data; scale if needed
|
||||
if scale == 1:
|
||||
fb = framebuf.FrameBuffer(pixels, len(pixels), 8, framebuf.MONO_VLSB)
|
||||
else:
|
||||
scaled_buf, new_w, new_h = self._scale_bitmap(pixels, len(pixels), 8, scale)
|
||||
fb = framebuf.FrameBuffer(scaled_buf, new_w, new_h, framebuf.MONO_VLSB)
|
||||
if just == 0:
|
||||
self.blit(fb, x, y0)
|
||||
elif just == 1:
|
||||
self.blit(fb, x - (len(pixels) * scale), y0)
|
||||
else:
|
||||
self.blit(fb, x - int((len(pixels) * scale) / 2), y0)
|
||||
pixels = bytearray([])
|
||||
if wrap == 0:
|
||||
return [x, y0 + (8 * scale) + 1]
|
||||
if wrap == 1:
|
||||
x = 0
|
||||
else:
|
||||
x = x0
|
||||
y0 = y0 + (8 * scale) + 1
|
||||
if y0 > self.height:
|
||||
return [x, y0]
|
||||
# Accumulate character bitmap data (unscaled)
|
||||
pixels += bytearray(cdata)
|
||||
|
||||
# Render any remaining accumulated text
|
||||
if col == 0:
|
||||
for j, v in enumerate(pixels):
|
||||
pixels[j] = 0xFF & ~v
|
||||
if scale == 1:
|
||||
fb = framebuf.FrameBuffer(pixels, len(pixels), 8, framebuf.MONO_VLSB)
|
||||
else:
|
||||
scaled_buf, new_w, new_h = self._scale_bitmap(pixels, len(pixels), 8, scale)
|
||||
fb = framebuf.FrameBuffer(scaled_buf, new_w, new_h, framebuf.MONO_VLSB)
|
||||
if just == 0:
|
||||
self.blit(fb, x, y0)
|
||||
elif just == 1:
|
||||
self.blit(fb, x - (len(pixels) * scale), y0)
|
||||
else:
|
||||
self.blit(fb, x - int((len(pixels) * scale) / 2), y0)
|
||||
return [x, y0 + (8 * scale) + 1]
|
||||
|
||||
# Shows the framebuffer contents on the display.
|
||||
# If no arguments are given, the full frame buffer is sent to the display.
|
||||
# startXPage: The horizontal page index to start the update. The X-axis is divided in 16 8-pixel 'pages' (0 ~ 15)
|
||||
# startYLine: The vertical line index to start the display update (0 ~ 63)
|
||||
# endXPage: The horizontal page index to end the update (excluding that page index) (1 ~ 16)
|
||||
# endYLine: The vertical line index to end the display update (excluding that line index) (1 ~ 64)
|
||||
def show(self, startXPage=0, startYLine=0, endXPage=16, endYLine=64):
|
||||
self.__validateShowArguments(startXPage, startYLine, endXPage, endYLine)
|
||||
doCustomPageAddressing = startXPage > 0 or endXPage < 16
|
||||
if not doCustomPageAddressing:
|
||||
self.write_cmd(0xB0)
|
||||
for yLine in range(startYLine, endYLine):
|
||||
columnSramAddress = 63 - yLine
|
||||
self.write_cmd(0x00 + (columnSramAddress & 0x0f))
|
||||
self.write_cmd(0x10 + (columnSramAddress >> 4))
|
||||
if doCustomPageAddressing:
|
||||
self.write_cmd(0xB0 + (startXPage & 0x0f))
|
||||
for num in range(startXPage, endXPage):
|
||||
self.write_data(self.buffer[yLine * 16 + num])
|
||||
|
||||
def __validateShowArguments(self, startXPage, startYLine, endXPage, endYLine):
|
||||
if not startYLine < endYLine:
|
||||
raise IndexError("show(...): startYLine (" + str(startYLine) +
|
||||
") must be smaller than endYLine (" + str(endYLine) + ").")
|
||||
if startYLine < 0 or startYLine > 63:
|
||||
raise IndexError("show(...): startYLine acceptable range is 0 ~ 63. Given: " + str(startYLine))
|
||||
if endYLine < 1 or endYLine > 64:
|
||||
raise IndexError("show(...): endYLine acceptable range is 1 ~ 64. Given: " + str(endYLine))
|
||||
if not startXPage < endXPage:
|
||||
raise IndexError("show(...): startXPage (" + str(startXPage) +
|
||||
") must be smaller than endXPage (" + str(endXPage) + ").")
|
||||
if startXPage < 0 or startXPage > 15:
|
||||
raise IndexError("show(...): startXPage acceptable range is 0 ~ 15. Given: " + str(startXPage))
|
||||
if endXPage < 1 or endXPage > 16:
|
||||
raise IndexError("show(...): endXPage acceptable range is 1 ~ 16. Given: " + str(endXPage))
|
||||
|
||||
def auto_text(self, s, col=0xffff):
|
||||
"""
|
||||
Automatically determine the largest integer scale factor such that the full
|
||||
string (with whole words kept on the same line) fits on the screen,
|
||||
and render it centered.
|
||||
|
||||
Parameters:
|
||||
s : The text string to render.
|
||||
col : Color (default white).
|
||||
"""
|
||||
# Maximum possible scale based on unscaled height (8 pixels per line)
|
||||
max_possible_scale = self.height // 8
|
||||
space_width = 3 # unscaled space width in pixels
|
||||
|
||||
# Try scales from the largest down to 1
|
||||
for scale in range(max_possible_scale, 0, -1):
|
||||
words = s.split(' ')
|
||||
scale_valid = True
|
||||
# Check that every individual word fits on one line at this scale
|
||||
for word in words:
|
||||
# Compute the word width using the font data
|
||||
word_width = sum(len(self.font[ord(c) - 32]) * scale for c in word if 32 <= ord(c) <= 127)
|
||||
if word_width > self.width:
|
||||
scale_valid = False
|
||||
break
|
||||
if not scale_valid:
|
||||
continue # Try a smaller scale
|
||||
|
||||
# Build lines without breaking words.
|
||||
lines = []
|
||||
current_line_words = []
|
||||
current_line_width = 0
|
||||
for word in words:
|
||||
# Calculate word width at current scale.
|
||||
word_width = sum(len(self.font[ord(c) - 32]) * scale for c in word if 32 <= ord(c) <= 127)
|
||||
additional_space = space_width * scale if current_line_words else 0
|
||||
if current_line_words and (current_line_width + additional_space + word_width > self.width):
|
||||
# Commit current line and start a new one
|
||||
lines.append(" ".join(current_line_words))
|
||||
current_line_words = [word]
|
||||
current_line_width = word_width
|
||||
else:
|
||||
if current_line_words:
|
||||
current_line_width += additional_space + word_width
|
||||
else:
|
||||
current_line_width += word_width
|
||||
current_line_words.append(word)
|
||||
if current_line_words:
|
||||
lines.append(" ".join(current_line_words))
|
||||
|
||||
# Check total height: each line is (8*scale) pixels high with a 1-pixel gap between lines.
|
||||
total_height = len(lines) * (8 * scale) + (len(lines) - 1)
|
||||
if total_height <= self.height:
|
||||
# This scale works
|
||||
break
|
||||
|
||||
# Center the text vertically.
|
||||
total_text_height = len(lines) * (8 * scale) + (len(lines) - 1)
|
||||
y_offset = (self.height - total_text_height) // 2
|
||||
|
||||
self.clear()
|
||||
|
||||
# Render each line centered horizontally.
|
||||
for line in lines:
|
||||
line_width = 0
|
||||
for char in line:
|
||||
if char == ' ':
|
||||
line_width += space_width * scale
|
||||
else:
|
||||
C = ord(char)
|
||||
if C < 32 or C > 127:
|
||||
C = 32
|
||||
line_width += len(self.font[C - 32]) * scale
|
||||
x_offset = (self.width - line_width) // 2
|
||||
self.text(line, x_offset, y_offset, col, wrap=0, just=0, scale=scale)
|
||||
y_offset += (8 * scale) + 1
|
||||
|
||||
def clear(self):
|
||||
self.fill(self.black)
|
||||
self.show()
|
||||
|
||||
|
||||
|
||||
def get():
|
||||
global display
|
||||
if display is None:
|
||||
display = OLED_1inch3_SPI()
|
||||
return display
|
||||
|
||||
|
||||
def test():
|
||||
display = get()
|
||||
display.clear()
|
||||
print("Running display tests")
|
||||
|
||||
# Test sequence to validate Xpage and YLine writing to the display
|
||||
display.fill(1)
|
||||
display.show()
|
||||
display.fill(0)
|
||||
display.show()
|
||||
|
||||
# Fill the framebuffer with white in specific regions:
|
||||
display.fill(1)
|
||||
display.show(0, 0, 16, 8) # Top horizontal bar
|
||||
display.show(0, 56, 16, 64) # Bottom horizontal bar
|
||||
display.show(0, 8, 1, 56) # Vertical bar on the left, partially drawn
|
||||
display.show(7, 24, 9, 40) # 16 x 16 pixel square in the middle
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
336
main.py
336
main.py
@ -1,44 +1,326 @@
|
||||
import network
|
||||
import socket
|
||||
import ntptime
|
||||
import time
|
||||
import weather_display
|
||||
import weather_requests
|
||||
from PicoOled13 import get
|
||||
from machine import Pin, RTC, Timer
|
||||
import urequests as requests
|
||||
import _thread
|
||||
|
||||
|
||||
# --- WiFi Connection Setup ---
|
||||
SSID = 'octopod' # Replace with your WiFi SSID
|
||||
PASSWORD = 'amniotic-duo-portfolio' # Replace with your WiFi Password
|
||||
# Weather API functions
|
||||
def get_weather_data(url):
|
||||
response = None
|
||||
try:
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return data
|
||||
else:
|
||||
print(f"Error: Status {response.status_code}")
|
||||
print(response.text)
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Request failed: {e}")
|
||||
return None
|
||||
finally:
|
||||
# Always close the response to free sockets
|
||||
if response:
|
||||
try:
|
||||
response.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
#Set current location
|
||||
|
||||
|
||||
|
||||
# Global variables
|
||||
latitude = 50.9097
|
||||
longitude = -1.4043
|
||||
today_forecast = None
|
||||
current_view = "simple"
|
||||
station = None # Global WiFi station
|
||||
display = None # Global display object
|
||||
weather_disp = None # Global weather display object
|
||||
setup_complete = False # Flag to indicate if setup is complete
|
||||
weather_update_in_progress = False # Flag to prevent multiple concurrent updates
|
||||
|
||||
# WiFi credentials
|
||||
SSID = 'octopod'
|
||||
PASSWORD = 'amniotic-duo-portfolio'
|
||||
|
||||
# Event flags for button presses
|
||||
update_requested = False
|
||||
view_change_requested = False
|
||||
|
||||
|
||||
station = network.WLAN(network.STA_IF)
|
||||
station.active(True)
|
||||
station.connect(SSID, PASSWORD)
|
||||
print("Connecting to WiFi...")
|
||||
# Check WiFi and reconnect if needed
|
||||
def ensure_wifi_connected():
|
||||
global station
|
||||
|
||||
if not station.isconnected():
|
||||
print("WiFi disconnected, reconnecting...")
|
||||
station.connect(SSID, PASSWORD)
|
||||
|
||||
# Wait for connection with timeout
|
||||
start_time = time.time()
|
||||
while not station.isconnected():
|
||||
if time.time() - start_time > 15: # 15 second timeout
|
||||
print("Failed to reconnect to WiFi")
|
||||
return False
|
||||
time.sleep(1)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
timeout = 10
|
||||
start_time = time.time()
|
||||
while not station.isconnected():
|
||||
if time.time() - start_time > timeout:
|
||||
print("Failed to connect to WiFi. Check your SSID and password.")
|
||||
break
|
||||
time.sleep(1)
|
||||
def update_weather():
|
||||
global today_forecast, weather_update_in_progress
|
||||
|
||||
if station.isconnected():
|
||||
ip = station.ipconfig("addr4")
|
||||
time.sleep(1)
|
||||
ipv6_addr = station.ipconfig("addr6")
|
||||
print("Connected to WiFi! IPv4 address:", ip)
|
||||
print("Connected to WiFi! IPv6 address:", ipv6_addr)
|
||||
else:
|
||||
print("WiFi connection not established. Restart and try again.")
|
||||
raise SystemExit
|
||||
|
||||
if weather_update_in_progress:
|
||||
print("Weather update already in progress, ignoring request")
|
||||
return
|
||||
|
||||
weather_update_in_progress = True
|
||||
|
||||
try:
|
||||
import network
|
||||
wlan = network.WLAN(network.STA_IF)
|
||||
if not wlan.active():
|
||||
wlan.active(True)
|
||||
if not wlan.isconnected():
|
||||
wlan.connect(SSID, PASSWORD)
|
||||
print("Reconnecting WiFi from thread...")
|
||||
start_time = time.time()
|
||||
while not wlan.isconnected():
|
||||
if time.time() - start_time > 10:
|
||||
print("Thread failed to connect WiFi")
|
||||
display.auto_text("WiFi error (thread)")
|
||||
return
|
||||
time.sleep(0.5)
|
||||
|
||||
display.auto_text("Updating weather...")
|
||||
print("Thread WiFi connected. IP:", wlan.ifconfig()[0])
|
||||
|
||||
print("Fetching daily forecast...")
|
||||
display.auto_text("Getting forecast...")
|
||||
daily_data = weather_requests.get_daily(latitude, longitude)
|
||||
if not daily_data:
|
||||
print("Failed to get daily data")
|
||||
display.auto_text("Error: daily data")
|
||||
return
|
||||
|
||||
print("Fetching hourly forecast...")
|
||||
hourly_data = weather_requests.get_hourly(latitude, longitude)
|
||||
if not hourly_data:
|
||||
print("Failed to get hourly data")
|
||||
display.auto_text("Error: hourly data")
|
||||
return
|
||||
|
||||
new_forecast = daily_data
|
||||
new_forecast['current_temp'] = hourly_data['hourly']['temperature_2m'][0]
|
||||
today_forecast = new_forecast
|
||||
print("Weather data updated successfully")
|
||||
|
||||
weather_disp.reset_display()
|
||||
|
||||
|
||||
display.auto_text("Weather OK")
|
||||
|
||||
except Exception as e:
|
||||
print("Weather thread exception:", e)
|
||||
display.auto_text("Update error")
|
||||
|
||||
finally:
|
||||
weather_update_in_progress = False
|
||||
|
||||
def trigger_weather_update():
|
||||
_thread.start_new_thread(update_weather, ())
|
||||
|
||||
# Update display based on current data
|
||||
def update_display():
|
||||
global display, weather_disp, today_forecast, current_view
|
||||
|
||||
if today_forecast:
|
||||
if current_view == "detailed":
|
||||
weather_disp.display_weather(today_forecast)
|
||||
else:
|
||||
weather_disp.display_simple_weather(today_forecast)
|
||||
else:
|
||||
display.auto_text("Press KEY0 to update")
|
||||
|
||||
|
||||
|
||||
weather = weather_requests.get_daily(latitude, longitude)
|
||||
def is_leap(year):
|
||||
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
|
||||
|
||||
print(weather_requests.get_today_forecast(weather))
|
||||
def days_in_month(year, month):
|
||||
if month == 2:
|
||||
return 29 if is_leap(year) else 28
|
||||
return [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 30, 31][month - 1]
|
||||
|
||||
def weekday(year, month, day):
|
||||
# Zeller’s congruence (0=Saturday, 1=Sunday, ..., 6=Friday)
|
||||
if month < 3:
|
||||
month += 12
|
||||
year -= 1
|
||||
q = day
|
||||
m = month
|
||||
K = year % 100
|
||||
J = year // 100
|
||||
return (q + 13*(m + 1)//5 + K + K//4 + J//4 + 5*J) % 7
|
||||
|
||||
def last_sunday(year, month):
|
||||
"""Return (day, hour) of the last Sunday in a given month."""
|
||||
dim = days_in_month(year, month)
|
||||
for d in range(dim, dim - 7, -1):
|
||||
if weekday(year, month, d) == 1: # Sunday
|
||||
return d
|
||||
return None # should never happen
|
||||
|
||||
def is_dst(year, month, day, hour=12):
|
||||
"""Returns True if the given UTC time is during UK DST (British Summer Time)."""
|
||||
# DST starts last Sunday of March at 1:00
|
||||
march_last_sunday = last_sunday(year, 3)
|
||||
dst_start = (3, march_last_sunday, 1)
|
||||
|
||||
# DST ends last Sunday of October at 2:00
|
||||
oct_last_sunday = last_sunday(year, 10)
|
||||
dst_end = (10, oct_last_sunday, 2)
|
||||
|
||||
current = (month, day, hour)
|
||||
|
||||
return dst_start <= current < dst_end
|
||||
|
||||
|
||||
# Main loop (synchronous version)
|
||||
def main_loop():
|
||||
global setup_complete, update_requested, view_change_requested
|
||||
|
||||
# Wait for setup to complete before proceeding
|
||||
while not setup_complete:
|
||||
time.sleep(0.1)
|
||||
|
||||
print("Main loop starting...")
|
||||
display.auto_text("Ready. Press KEY0 to update")
|
||||
|
||||
update_weather() # initial call
|
||||
last_weather = time.ticks_ms()
|
||||
WEATHER_MS = 10 * 60 * 1000 # 10 min in milliseconds
|
||||
|
||||
while True:
|
||||
update_display()
|
||||
|
||||
# Has 10 minutes elapsed?
|
||||
if time.ticks_diff(time.ticks_ms(), last_weather) >= WEATHER_MS:
|
||||
update_weather()
|
||||
last_weather = time.ticks_ms()
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
|
||||
# Setup function
|
||||
def setup():
|
||||
global station, display, weather_disp, setup_complete
|
||||
|
||||
# --- WiFi Connection Setup ---
|
||||
station = network.WLAN(network.STA_IF)
|
||||
station.active(True)
|
||||
station.connect(SSID, PASSWORD)
|
||||
print("Connecting to WiFi...")
|
||||
|
||||
timeout = 15 # Extended timeout
|
||||
start_time = time.time()
|
||||
while not station.isconnected():
|
||||
if time.time() - start_time > timeout:
|
||||
print("Failed to connect to WiFi. Check your SSID and password.")
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
if station.isconnected():
|
||||
ip = station.ifconfig()[0] # Standard way to get IP
|
||||
print("Connected to WiFi! IP address:", ip)
|
||||
|
||||
# Set up the RTC using NTP
|
||||
rtc = RTC()
|
||||
try:
|
||||
print("Syncing time with NTP server...")
|
||||
# Retry NTP time sync
|
||||
for _ in range(3):
|
||||
try:
|
||||
ntptime.settime()
|
||||
break
|
||||
except:
|
||||
print("NTP retry...")
|
||||
time.sleep(1)
|
||||
|
||||
# Get current time from RTC
|
||||
current_time = list(rtc.datetime())
|
||||
year = current_time[0]
|
||||
month = current_time[1]
|
||||
day = current_time[2]
|
||||
hour = current_time[4]
|
||||
|
||||
# Adjust for UK time (UTC+0/+1)
|
||||
if is_dst(year, month, day, hour):
|
||||
print("British Summer Time (BST) is active - adding 1 hour")
|
||||
current_time[4] += 1
|
||||
if current_time[4] >= 24:
|
||||
current_time[4] -= 24
|
||||
current_time[2] += 1
|
||||
|
||||
# Update RTC with the adjusted time
|
||||
rtc.datetime(tuple(current_time))
|
||||
print("RTC set with UK local time:", rtc.datetime())
|
||||
except OSError as e:
|
||||
print("Error syncing time:", e)
|
||||
else:
|
||||
print("WiFi connection not established. Restart and try again.")
|
||||
raise SystemExit
|
||||
|
||||
# Initialize display
|
||||
display = get()
|
||||
weather_disp = weather_display.WeatherDisplay(display)
|
||||
|
||||
# Setup button interrupts
|
||||
button0 = Pin(display.KEY0, Pin.IN, Pin.PULL_UP)
|
||||
button1 = Pin(display.KEY1, Pin.IN, Pin.PULL_UP)
|
||||
button0.irq(trigger=Pin.IRQ_FALLING, handler=detailed_view)
|
||||
button1.irq(trigger=Pin.IRQ_FALLING, handler=simple_view)
|
||||
|
||||
# Display initial message
|
||||
display.auto_text("Starting weather display...")
|
||||
|
||||
# Set flag to indicate setup is complete
|
||||
setup_complete = True
|
||||
|
||||
print("Setup complete!")
|
||||
|
||||
|
||||
def detailed_view(pin):
|
||||
global current_view
|
||||
print("Detailed view")
|
||||
current_view = "detailed"
|
||||
|
||||
def simple_view(pin):
|
||||
global current_view
|
||||
print("Simple view")
|
||||
current_view = "simple"
|
||||
|
||||
# Main function
|
||||
def main():
|
||||
# Run setup
|
||||
setup()
|
||||
|
||||
# Start the main loop
|
||||
main_loop()
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
# Run the main program
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
262
weather_display.py
Normal file
262
weather_display.py
Normal file
@ -0,0 +1,262 @@
|
||||
import time
|
||||
import uasyncio as asyncio
|
||||
|
||||
|
||||
class WeatherDisplay:
|
||||
def __init__(self, display):
|
||||
self.display = display
|
||||
self.text_padding = 4
|
||||
self.last_displayed_data = None
|
||||
self.time_update_running = False
|
||||
self.time_thread = None
|
||||
|
||||
def weathercode_to_text(self, weathercode):
|
||||
# More detailed weather code interpretations
|
||||
if weathercode == 0:
|
||||
return 'Clear Sky'
|
||||
elif weathercode == 1:
|
||||
return 'Mainly Clear'
|
||||
elif weathercode == 2:
|
||||
return 'Partly Cloudy'
|
||||
elif weathercode == 3:
|
||||
return 'Overcast'
|
||||
elif weathercode in [45, 48]:
|
||||
return 'Foggy'
|
||||
elif weathercode in [51, 53, 55]:
|
||||
return 'Light Rain'
|
||||
elif weathercode in [56, 57]:
|
||||
return 'Freezing Rain'
|
||||
elif weathercode in [61, 63, 65]:
|
||||
return 'Moderate Rain'
|
||||
elif weathercode in [66, 67]:
|
||||
return 'Heavy Rain'
|
||||
elif weathercode in [71, 73, 75]:
|
||||
return 'Snow Fall'
|
||||
elif weathercode == 77:
|
||||
return 'Snow Grains'
|
||||
elif weathercode in [80, 81, 82]:
|
||||
return 'Rain Showers'
|
||||
elif weathercode in [85, 86]:
|
||||
return 'Snow Showers'
|
||||
elif weathercode == 95:
|
||||
return 'Thunderstorm'
|
||||
elif weathercode in [96, 99]:
|
||||
return 'Thunderstorm with Hail'
|
||||
else:
|
||||
return 'Unknown Weather'
|
||||
|
||||
|
||||
if force_full_update:
|
||||
self.display.clear()
|
||||
else:
|
||||
# Create a clean buffer but don't send to display yet
|
||||
self.display.fill(self.display.black)
|
||||
|
||||
# Text starts at left padding
|
||||
text_x = self.text_padding
|
||||
text_y = 8
|
||||
|
||||
# Track regions that need updating
|
||||
update_regions = []
|
||||
|
||||
# Display temperature
|
||||
temp_text = f"Temp: {weather_data.get('max_temp', 0):.0f}/{weather_data.get('min_temp', 0):.0f}C"
|
||||
if force_full_update or not self.last_displayed_data or \
|
||||
self.last_displayed_data.get('max_temp') != weather_data.get('max_temp') or \
|
||||
self.last_displayed_data.get('miget_wen_temp') != weather_data.get('min_temp'):
|
||||
self.display.text(temp_text, text_x, text_y)
|
||||
update_regions.append((0, text_y, 16, text_y + 8))
|
||||
|
||||
# Display weather description
|
||||
weathercode = weather_data.get('weathercode', 0)
|
||||
weather_text = self.weathercode_to_text(weathercode)
|
||||
if force_full_update or not self.last_displayed_data or \
|
||||
self.last_displayed_data.get('weathercode') != weathercode:
|
||||
self.display.text(f"{weather_text}", text_x, text_y + 12)
|
||||
update_regions.append((0, text_y + 12, 16, text_y + 20))
|
||||
|
||||
# Display precipitation
|
||||
precip_text = f"Precip: {weather_data.get('precip_mm', 0):.1f}mm"
|
||||
if force_full_update or not self.last_displayed_data or \
|
||||
self.last_displayed_data.get('precip_mm') != weather_data.get('precip_mm'):
|
||||
self.display.text(precip_text, text_x, text_y + 24)
|
||||
update_regions.append((0, text_y + 24, 16, text_y + 32))
|
||||
|
||||
# Display date
|
||||
date_text = weather_data.get('date', '')
|
||||
if date_text and len(date_text) >= 10:
|
||||
year = date_text[0:4]
|
||||
month = date_text[5:7]
|
||||
day = date_text[8:10]
|
||||
date_text = f"{day}-{month}-{year}"
|
||||
if force_full_update or not self.last_displayed_data or \
|
||||
self.last_displayed_data.get('date') != weather_data.get('date'):
|
||||
self.display.text(f"Date: {date_text}", text_x, text_y + 36)
|
||||
update_regions.append((0, text_y + 36, 16, text_y + 44))
|
||||
|
||||
# Store current data for future comparison
|
||||
self.last_displayed_data = dict(weather_data)
|
||||
self.last_displayed_data['view_type'] = 'detailed'
|
||||
|
||||
# Update display - either full or partial updates
|
||||
if force_full_update:
|
||||
self.display.show()
|
||||
else:
|
||||
# Merge overlapping regions for more efficient updates
|
||||
if update_regions:
|
||||
# Simple approach: just update the full range covered by all regions
|
||||
min_y = min(region[1] for region in update_regions)
|
||||
max_y = max(region[3] for region in update_regions)
|
||||
self.display.show(0, min_y, 16, max_y)
|
||||
else:
|
||||
# No changes detected, no need to update
|
||||
pass
|
||||
|
||||
|
||||
def display_weather(self, weather_data):
|
||||
"""Detailed view with max/min temps, description, precip, date."""
|
||||
# If this is the full API response, extract today's values:
|
||||
if 'daily' in weather_data:
|
||||
d = weather_data['daily']
|
||||
weather_data = {
|
||||
'max_temp': d['temperature_2m_max'][0],
|
||||
'min_temp': d['temperature_2m_min'][0],
|
||||
'weathercode':d['weathercode'][0],
|
||||
'precip_mm': d['precipitation_sum'][0],
|
||||
'date': d['time'][0],
|
||||
}
|
||||
|
||||
force_full_update = (
|
||||
self.last_displayed_data is None or
|
||||
self.last_displayed_data.get('view_type') != 'detailed'
|
||||
)
|
||||
|
||||
if force_full_update:
|
||||
self.display.clear()
|
||||
else:
|
||||
self.display.fill(self.display.black)
|
||||
|
||||
text_x = self.text_padding
|
||||
text_y = 8
|
||||
update_regions = []
|
||||
|
||||
# Temperature
|
||||
temp_text = f"Temp: {weather_data.get('max_temp', 0):.0f}/" \
|
||||
f"{weather_data.get('min_temp', 0):.0f}C"
|
||||
if (force_full_update
|
||||
or self.last_displayed_data.get('max_temp') != weather_data.get('max_temp')
|
||||
or self.last_displayed_data.get('min_temp') != weather_data.get('min_temp')
|
||||
):
|
||||
self.display.text(temp_text, text_x, text_y)
|
||||
update_regions.append((text_y, text_y + 8))
|
||||
|
||||
# Description
|
||||
code = weather_data.get('weathercode', 0)
|
||||
desc = self.weathercode_to_text(code)
|
||||
if (force_full_update
|
||||
or self.last_displayed_data.get('weathercode') != code
|
||||
):
|
||||
self.display.text(desc, text_x, text_y + 12)
|
||||
update_regions.append((text_y + 12, text_y + 20))
|
||||
|
||||
# Precip
|
||||
precip = f"Precip: {weather_data.get('precip_mm', 0):.1f}mm"
|
||||
if (force_full_update
|
||||
or self.last_displayed_data.get('precip_mm') != weather_data.get('precip_mm')
|
||||
):
|
||||
self.display.text(precip, text_x, text_y + 24)
|
||||
update_regions.append((text_y + 24, text_y + 32))
|
||||
|
||||
# Date (YYYY-MM-DD → DD-MM-YYYY)
|
||||
raw_date = weather_data.get('date', '')
|
||||
if raw_date and len(raw_date) >= 10:
|
||||
dd = raw_date[8:10]; mm = raw_date[5:7]; yyyy = raw_date[0:4]
|
||||
formatted = f"Date: {dd}-{mm}-{yyyy}"
|
||||
if (force_full_update
|
||||
or self.last_displayed_data.get('date') != raw_date
|
||||
):
|
||||
self.display.text(formatted, text_x, text_y + 36)
|
||||
update_regions.append((text_y + 36, text_y + 44))
|
||||
|
||||
# Save for next time
|
||||
self.last_displayed_data = dict(weather_data)
|
||||
self.last_displayed_data['view_type'] = 'detailed'
|
||||
|
||||
# If full, just show everything
|
||||
if force_full_update:
|
||||
self.display.show()
|
||||
return
|
||||
|
||||
# Otherwise do a partial update over all pages
|
||||
if update_regions:
|
||||
min_y = min(r[0] for r in update_regions)
|
||||
max_y = max(r[1] for r in update_regions)
|
||||
page_count = self.display.height // 8
|
||||
self.display.show(1, min_y, page_count, max_y)
|
||||
# else: nothing changed
|
||||
|
||||
|
||||
|
||||
def display_simple_weather(self, weather_data):
|
||||
force_full_update = self.last_displayed_data is None or self.last_displayed_data.get('view_type') != 'simple'
|
||||
|
||||
if force_full_update:
|
||||
self.display.clear()
|
||||
else:
|
||||
# Create a clean buffer but don't send to display yet
|
||||
self.display.fill(self.display.black)
|
||||
|
||||
# Text starts at left padding
|
||||
text_x = self.text_padding
|
||||
center_y = self.display.height // 2
|
||||
|
||||
# Track regions that need updating
|
||||
update_regions = []
|
||||
|
||||
# Display temperature large
|
||||
temp_text = f"{weather_data.get('current_temp', 0):.0f}C"
|
||||
if force_full_update or not self.last_displayed_data or \
|
||||
self.last_displayed_data.get('current_temp') != weather_data.get('current_temp'):
|
||||
self.display.text(temp_text, text_x, center_y - 8, scale=2)
|
||||
update_regions.append((0, center_y - 8, 16, center_y + 8)) # Scaled text is 16px high
|
||||
|
||||
# Display description
|
||||
weather_desc = self.weathercode_to_text(weather_data.get('weathercode', 0))
|
||||
if force_full_update or not self.last_displayed_data or \
|
||||
self.last_displayed_data.get('weathercode') != weather_data.get('weathercode'):
|
||||
self.display.text(weather_desc, text_x, center_y + 12)
|
||||
update_regions.append((0, center_y + 12, 16, center_y + 20))
|
||||
|
||||
|
||||
|
||||
# Display current time
|
||||
current_time_tuple = time.localtime(time.time())
|
||||
current_time = f"{current_time_tuple[3]:02d}:{current_time_tuple[4]:02d}:{current_time_tuple[5]:02d}"
|
||||
if current_time and (force_full_update or not self.last_displayed_data or \
|
||||
self.last_displayed_data.get('current_time') != current_time):
|
||||
self.display.text(current_time, text_x, center_y + 24)
|
||||
update_regions.append((0, center_y + 24, 16, center_y + 32))
|
||||
|
||||
# Store current data for future comparison
|
||||
self.last_displayed_data = dict(weather_data)
|
||||
self.last_displayed_data['view_type'] = 'simple'
|
||||
|
||||
# Update display - either full or partial updates
|
||||
if force_full_update:
|
||||
self.display.show()
|
||||
else:
|
||||
# Merge overlapping regions for more efficient updates
|
||||
if update_regions:
|
||||
# Simple approach: just update the full range covered by all regions
|
||||
min_y = min(region[1] for region in update_regions)
|
||||
max_y = max(region[3] for region in update_regions)
|
||||
self.display.show(0, min_y, 16, max_y)
|
||||
else:
|
||||
# No changes detected, no need to update
|
||||
pass
|
||||
|
||||
def reset_display(self):
|
||||
"""Clear display and reset last displayed data."""
|
||||
self.display.clear()
|
||||
self.display.show()
|
||||
self.last_displayed_data = None
|
||||
Loading…
x
Reference in New Issue
Block a user