Created
July 28, 2024 16:02
-
-
Save Ultra980/047359b1cf4a937cc51a6a163a12c1d8 to your computer and use it in GitHub Desktop.
Raspberry Pi Pico W SSD1306 OLED clock, with NTP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from machine import Pin, I2C, RTC | |
import time | |
import ssd1306 | |
from random import randint | |
import ntp | |
# Don't forget to change these values! | |
# I recommend connecting the sda and scl to hardware-controlled I2C pins on your pico. | |
# You can look up the pinout and see which pins are controlled by which controller | |
# In my case, pins 0 and 1 (confusingly labeled 1 and 2 on the pi), the two on the top left, | |
# are controlled by controller 0 | |
i2c = I2C(sda=Pin(0), scl=Pin(1), id=0) | |
# You should change these values to the resolution of your display, | |
oled_width = 128 | |
oled_height = 32 | |
# ...and follow the instructions on https://randomnerdtutorials.com/raspberry-pi-pico-ssd1306-oled-micropython/ | |
# to check if your display has a different address, and if so, how to set it | |
oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c) | |
rtc = machine.RTC() | |
ntpobj = ntp.ntp(ssid = "Not giving you", password = "my credentials ;)", timezone = "Etc/UTC") # Change accordingly | |
ntpobj.set_time() | |
while True: | |
(year, month, day, idk, hour, minute, second, idk) = rtc.datetime() | |
oled.fill(0) | |
oled.text('%02d:%02d:%02d' %(hour, minute, second), 20, 15) # should be easy to modify for 12h time | |
oled.show() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# I got the majority of this code from https://gist.githubusercontent.com/aallan/581ecf4dc92cd53e3a415b7c33a1147c/raw/15403db8029f13db5b6c24c88be3cb9787a6a258/picow_ntp_client.py, | |
# turned it into a class and added timezones to it | |
import network | |
import socket | |
import time | |
import struct | |
import machine | |
from machine import Pin | |
import urequests | |
class ntp: | |
def __init__(self, ssid, password, delta = 2208988800, host = "pool.ntp.org", timezone = 'Etc/UTC'): | |
# Set the local variables | |
self.NTP_DELTA = delta | |
self.host = host | |
self.ssid = ssid | |
self.password = password | |
self.timezone = timezone | |
# Enable the WLAN connection | |
wlan = network.WLAN(network.STA_IF) | |
wlan.active(True) | |
wlan.connect(ssid, password) | |
# Try to connect to the WiFi network | |
max_wait = 10 | |
while max_wait > 0: | |
if wlan.status() < 0 or wlan.status() >= 3: | |
break | |
max_wait -= 1 | |
print('waiting for connection...') | |
time.sleep(1) | |
if wlan.status() != 3: | |
raise RuntimeError('network connection failed') | |
else: | |
print('connected') | |
status = wlan.ifconfig() | |
print( 'ip = ' + status[0] ) | |
# Gets the timezone offset from UTC/GMT | |
# TODO: find a way to directly get the time for our timezone | |
def get_timezone_offset(self): | |
print("Performing worldtimeapi request") | |
response = urequests.get(f'http://worldtimeapi.org/api/timezone/{self.timezone}') | |
if response.status_code == 200: | |
print("Request finished succesfully") | |
data = response.json() | |
offset = data['raw_offset'] + data['dst_offset'] | |
return offset | |
else: | |
raise RuntimeError('Failed to get timezone information') | |
def set_time(self): | |
# Sets the NTP query bytes | |
NTP_QUERY = bytearray(48) | |
NTP_QUERY[0] = 0x1B # I don't have a graybeard yet, so I don't know what this byte means | |
# OK, after reading more about it (read: ChatGPT just told me), I | |
# found out it's just the version or something | |
# Initializes the socket | |
addr = socket.getaddrinfo(self.host, 123)[0][-1] | |
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
try: | |
s.settimeout(1) | |
res = s.sendto(NTP_QUERY, addr) | |
msg = s.recv(48) | |
finally: | |
s.close() | |
# Turns the returned data into something we can use | |
val = struct.unpack("!I", msg[40:44])[0] | |
# Gets the timezone offset | |
timezone_offset = self.get_timezone_offset() | |
t = val - self.NTP_DELTA + timezone_offset | |
tm = time.gmtime(t) | |
# Honestly, I'm not sure what some of these values are | |
machine.RTC().datetime((tm[0], tm[1], tm[2], tm[6] + 1, tm[3], tm[4], tm[5], 0)) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# I copied this from https://raw.githubusercontent.com/RuiSantosdotme/ESP-MicroPython/master/code/Others/OLED/ssd1306.py, | |
# which I found on https://randomnerdtutorials.com/raspberry-pi-pico-ssd1306-oled-micropython/ | |
# The lines after this one are the original, unmodified code: | |
# MicroPython SSD1306 OLED driver, I2C and SPI interfaces created by Adafruit | |
import time | |
import framebuf | |
# register definitions | |
SET_CONTRAST = const(0x81) | |
SET_ENTIRE_ON = const(0xa4) | |
SET_NORM_INV = const(0xa6) | |
SET_DISP = const(0xae) | |
SET_MEM_ADDR = const(0x20) | |
SET_COL_ADDR = const(0x21) | |
SET_PAGE_ADDR = const(0x22) | |
SET_DISP_START_LINE = const(0x40) | |
SET_SEG_REMAP = const(0xa0) | |
SET_MUX_RATIO = const(0xa8) | |
SET_COM_OUT_DIR = const(0xc0) | |
SET_DISP_OFFSET = const(0xd3) | |
SET_COM_PIN_CFG = const(0xda) | |
SET_DISP_CLK_DIV = const(0xd5) | |
SET_PRECHARGE = const(0xd9) | |
SET_VCOM_DESEL = const(0xdb) | |
SET_CHARGE_PUMP = const(0x8d) | |
class SSD1306: | |
def __init__(self, width, height, external_vcc): | |
self.width = width | |
self.height = height | |
self.external_vcc = external_vcc | |
self.pages = self.height // 8 | |
# Note the subclass must initialize self.framebuf to a framebuffer. | |
# This is necessary because the underlying data buffer is different | |
# between I2C and SPI implementations (I2C needs an extra byte). | |
self.poweron() | |
self.init_display() | |
def init_display(self): | |
for cmd in ( | |
SET_DISP | 0x00, # off | |
# address setting | |
SET_MEM_ADDR, 0x00, # horizontal | |
# resolution and layout | |
SET_DISP_START_LINE | 0x00, | |
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0 | |
SET_MUX_RATIO, self.height - 1, | |
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0 | |
SET_DISP_OFFSET, 0x00, | |
SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12, | |
# timing and driving scheme | |
SET_DISP_CLK_DIV, 0x80, | |
SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1, | |
SET_VCOM_DESEL, 0x30, # 0.83*Vcc | |
# display | |
SET_CONTRAST, 0xff, # maximum | |
SET_ENTIRE_ON, # output follows RAM contents | |
SET_NORM_INV, # not inverted | |
# charge pump | |
SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14, | |
SET_DISP | 0x01): # on | |
self.write_cmd(cmd) | |
self.fill(0) | |
self.show() | |
def poweroff(self): | |
self.write_cmd(SET_DISP | 0x00) | |
def contrast(self, contrast): | |
self.write_cmd(SET_CONTRAST) | |
self.write_cmd(contrast) | |
def invert(self, invert): | |
self.write_cmd(SET_NORM_INV | (invert & 1)) | |
def show(self): | |
x0 = 0 | |
x1 = self.width - 1 | |
if self.width == 64: | |
# displays with width of 64 pixels are shifted by 32 | |
x0 += 32 | |
x1 += 32 | |
self.write_cmd(SET_COL_ADDR) | |
self.write_cmd(x0) | |
self.write_cmd(x1) | |
self.write_cmd(SET_PAGE_ADDR) | |
self.write_cmd(0) | |
self.write_cmd(self.pages - 1) | |
self.write_framebuf() | |
def fill(self, col): | |
self.framebuf.fill(col) | |
def pixel(self, x, y, col): | |
self.framebuf.pixel(x, y, col) | |
def scroll(self, dx, dy): | |
self.framebuf.scroll(dx, dy) | |
def text(self, string, x, y, col=1): | |
self.framebuf.text(string, x, y, col) | |
class SSD1306_I2C(SSD1306): | |
def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False): | |
self.i2c = i2c | |
self.addr = addr | |
self.temp = bytearray(2) | |
# Add an extra byte to the data buffer to hold an I2C data/command byte | |
# to use hardware-compatible I2C transactions. A memoryview of the | |
# buffer is used to mask this byte from the framebuffer operations | |
# (without a major memory hit as memoryview doesn't copy to a separate | |
# buffer). | |
self.buffer = bytearray(((height // 8) * width) + 1) | |
self.buffer[0] = 0x40 # Set first byte of data buffer to Co=0, D/C=1 | |
self.framebuf = framebuf.FrameBuffer1(memoryview(self.buffer)[1:], width, height) | |
super().__init__(width, height, external_vcc) | |
def write_cmd(self, cmd): | |
self.temp[0] = 0x80 # Co=1, D/C#=0 | |
self.temp[1] = cmd | |
self.i2c.writeto(self.addr, self.temp) | |
def write_framebuf(self): | |
# Blast out the frame buffer using a single I2C transaction to support | |
# hardware I2C interfaces. | |
self.i2c.writeto(self.addr, self.buffer) | |
def poweron(self): | |
pass | |
class SSD1306_SPI(SSD1306): | |
def __init__(self, width, height, spi, dc, res, cs, external_vcc=False): | |
self.rate = 10 * 1024 * 1024 | |
dc.init(dc.OUT, value=0) | |
res.init(res.OUT, value=0) | |
cs.init(cs.OUT, value=1) | |
self.spi = spi | |
self.dc = dc | |
self.res = res | |
self.cs = cs | |
self.buffer = bytearray((height // 8) * width) | |
self.framebuf = framebuf.FrameBuffer1(self.buffer, width, height) | |
super().__init__(width, height, external_vcc) | |
def write_cmd(self, cmd): | |
self.spi.init(baudrate=self.rate, polarity=0, phase=0) | |
self.cs.high() | |
self.dc.low() | |
self.cs.low() | |
self.spi.write(bytearray([cmd])) | |
self.cs.high() | |
def write_framebuf(self): | |
self.spi.init(baudrate=self.rate, polarity=0, phase=0) | |
self.cs.high() | |
self.dc.high() | |
self.cs.low() | |
self.spi.write(self.buffer) | |
self.cs.high() | |
def poweron(self): | |
self.res.high() | |
time.sleep_ms(1) | |
self.res.low() | |
time.sleep_ms(10) | |
self.res.high() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment