Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52530ab2ff | |||
| 8b4167f7cc | |||
| d1a017a068 | |||
| 7c73e7cf97 | |||
| 8467a3cc50 | |||
| 9545c01150 | |||
| 0d8178375b | |||
| 7a653c424b | |||
| ec63c2b2d2 | |||
| ab24316156 |
@@ -10,7 +10,7 @@ A [MicroPython](https://micropython.org/) app to display local time on an SSD130
|
|||||||
The following hardware is what I used for a deployment:
|
The following hardware is what I used for a deployment:
|
||||||
|
|
||||||
* Raspberry Pi Pico 2 W: https://www.amazon.com/dp/B0DRJXPPWL
|
* Raspberry Pi Pico 2 W: https://www.amazon.com/dp/B0DRJXPPWL
|
||||||
* The **W** is VERY important, as without networking it can't reach Home Assistant!
|
* The **W** is VERY important, as without networking it can't reach NTP servers!
|
||||||
* Also a Raspberry Pi Pico W *should* work too, but I haven't tested it yet
|
* Also a Raspberry Pi Pico W *should* work too, but I haven't tested it yet
|
||||||
* Elegoo SSD1306 LCD: https://www.amazon.com/dp/B0FSRQG23K
|
* Elegoo SSD1306 LCD: https://www.amazon.com/dp/B0FSRQG23K
|
||||||
|
|
||||||
@@ -41,14 +41,15 @@ These are not necessary to run the app, but can be useful for testing and/or fla
|
|||||||
1. Connect your SSD1306 display to your Pico.
|
1. Connect your SSD1306 display to your Pico.
|
||||||
2. Connect your Pico to your PC via a micro-USB to USB type-A cable.
|
2. Connect your Pico to your PC via a micro-USB to USB type-A cable.
|
||||||
3. [Flash your Raspberry Pi Pico with MicroPython](https://docs.micropython.org/en/latest/rp2/tutorial/reset.html).
|
3. [Flash your Raspberry Pi Pico with MicroPython](https://docs.micropython.org/en/latest/rp2/tutorial/reset.html).
|
||||||
a. This has been tested with MicroPython 1.29, but other versions should work.
|
1. This has been tested with MicroPython 1.29, but other versions should work.
|
||||||
4. Clone this git repository: `git clone https://git.metaunix.net/BitGoblin/pico-time`.
|
4. Clone this git repository: `git clone https://git.metaunix.net/BitGoblin/pico-time`.
|
||||||
5. `cd` into the repo: `cd pico-time`.
|
5. `cd` into the repo: `cd pico-time`.
|
||||||
6. Copy `config.example.py` to `config.py` and edit the settings to match your environment.
|
6. Install Python dependencies via mpremote: `xargs mpremote mip install < requirements.txt`.
|
||||||
7. Copy the Python code over to the Pico:
|
7. Copy `config.example.py` to `config.py` and edit the settings to match your environment.
|
||||||
a. `mpremote cp main.py :main.py`
|
8. Copy the Python code over to the Pico:
|
||||||
b. `mpremote cp config.py :config.py`
|
1. `mpremote cp main.py :main.py`
|
||||||
8. Reset the Pico: `mpremote reset`
|
2. `mpremote cp config.py :config.py`
|
||||||
|
9. Reset the Pico: `mpremote reset`
|
||||||
|
|
||||||
If it worked you should see the display show a message about connecting to WiFi, then syncing time and finally displaying the local time. If it failed, you can monitor the app via `mpremote repl` or using an IDE like [Thonny](https://thonny.org/).
|
If it worked you should see the display show a message about connecting to WiFi, then syncing time and finally displaying the local time. If it failed, you can monitor the app via `mpremote repl` or using an IDE like [Thonny](https://thonny.org/).
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -2,5 +2,11 @@
|
|||||||
WIFI_SSID = "YourSSID" # Set this to your Wi-Fi network name
|
WIFI_SSID = "YourSSID" # Set this to your Wi-Fi network name
|
||||||
WIFI_PASSWORD = "YourPassword" # Set this to your Wi-Fi network password
|
WIFI_PASSWORD = "YourPassword" # Set this to your Wi-Fi network password
|
||||||
|
|
||||||
# Timezone settings
|
# Display Settings
|
||||||
|
DISPLAY_SDA_PIN = 4 # Default SDA pin is GP4
|
||||||
|
DISPLAY_SCL_PIN = 5 # Default SCL pin is GP5
|
||||||
|
|
||||||
|
# Time settings
|
||||||
|
USE_24_HOUR = False # Set this to True for 24-hour time (no AM/PM)
|
||||||
UTC_OFFSET = -5 # Set this to your desired NTP server
|
UTC_OFFSET = -5 # Set this to your desired NTP server
|
||||||
|
DST_OBSERVED = True # Set this to false if you don't observe Daylight Savings Time
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import framebuf
|
||||||
import network
|
import network
|
||||||
import ntptime
|
import ntptime
|
||||||
import time
|
import time
|
||||||
@@ -14,7 +15,7 @@ except ImportError:
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
# --- Hardware Setup ---
|
# --- Hardware Setup ---
|
||||||
i2c = I2C(0, sda=Pin(16), scl=Pin(17), freq=400000)
|
i2c = I2C(0, sda=Pin(Config.DISPLAY_SDA_PIN), scl=Pin(Config.DISPLAY_SCL_PIN), freq=400000)
|
||||||
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
|
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
|
||||||
|
|
||||||
|
|
||||||
@@ -51,21 +52,129 @@ except Exception as e:
|
|||||||
# Disconnect Wi-Fi to conserve power once time is synchronized
|
# Disconnect Wi-Fi to conserve power once time is synchronized
|
||||||
wlan.active(False)
|
wlan.active(False)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_scaled_text(display, text, x, y, scale=2):
|
||||||
|
"""
|
||||||
|
Renders text scaled up by an integer factor (scale=2 -> 16x16, scale=3 -> 24x24).
|
||||||
|
Uses a temporary 1-bit FrameBuffer to capture the default 8x8 font.
|
||||||
|
"""
|
||||||
|
width = len(text) * 8
|
||||||
|
height = 8
|
||||||
|
|
||||||
|
# 1-bit MONO_HLSB buffer: (width * height) / 8 bytes
|
||||||
|
buf = bytearray((width * height) // 8)
|
||||||
|
fb = framebuf.FrameBuffer(buf, width, height, framebuf.MONO_HLSB)
|
||||||
|
|
||||||
|
# Render string with built-in font into the scratch buffer
|
||||||
|
fb.fill(0)
|
||||||
|
fb.text(text, 0, 0, 1)
|
||||||
|
|
||||||
|
# Read each pixel and draw a (scale x scale) block on the OLED
|
||||||
|
for r in range(height):
|
||||||
|
for c in range(width):
|
||||||
|
if fb.pixel(c, r):
|
||||||
|
display.fill_rect(x + (c * scale), y + (r * scale), scale, scale, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_x_spacing(text, scale=1):
|
||||||
|
"""
|
||||||
|
Calculates the amount of horizontal (x) spacing is needed to center a string.
|
||||||
|
"""
|
||||||
|
scaled_text = len(text) * scale
|
||||||
|
return (128 - (scaled_text * 8)) // 2
|
||||||
|
|
||||||
|
|
||||||
|
def is_dst(year, month, day, hour_utc, weekday):
|
||||||
|
"""
|
||||||
|
Returns True if UTC time falls within US/Canada Daylight Saving Time:
|
||||||
|
- Starts: 2nd Sunday in March at 02:00 local (07:00 UTC for UTC-5)
|
||||||
|
- Ends: 1st Sunday in November at 02:00 local (06:00 UTC for UTC-4)
|
||||||
|
Note: weekday is 0=Monday ... 6=Sunday.
|
||||||
|
"""
|
||||||
|
if not Config.DST_OBSERVED:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 1. Months completely inside or outside DST
|
||||||
|
if month < 3 or month > 11:
|
||||||
|
return False
|
||||||
|
if 3 < month < 11:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 2. March (Starts 2nd Sunday)
|
||||||
|
# The 2nd Sunday always falls between March 8 and March 14
|
||||||
|
if month == 3:
|
||||||
|
# Find the day of the 2nd Sunday
|
||||||
|
second_sunday = 14 - ((weekday - day + 14) % 7)
|
||||||
|
if day < second_sunday:
|
||||||
|
return False
|
||||||
|
elif day > second_sunday:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# On the transition day itself, 02:00 EST is 07:00 UTC
|
||||||
|
return hour_utc >= 7
|
||||||
|
|
||||||
|
# 3. November (Ends 1st Sunday)
|
||||||
|
# The 1st Sunday always falls between Nov 1 and Nov 7
|
||||||
|
if month == 11:
|
||||||
|
first_sunday = 7 - ((weekday - day + 7) % 7)
|
||||||
|
if day < first_sunday:
|
||||||
|
return True
|
||||||
|
elif day > first_sunday:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# On the transition day itself, 02:00 EDT (01:00 EST) is 06:00 UTC
|
||||||
|
return hour_utc < 6
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# --- Main Loop ---
|
# --- Main Loop ---
|
||||||
DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||||
|
TIME_Y = 12 # Keep vertical placement consistent
|
||||||
|
|
||||||
|
last_second = -1
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Apply UTC offset to local epoch
|
# 1. Fast, single-integer read from RTC
|
||||||
local_epoch = time.time() + (Config.UTC_OFFSET * 3600)
|
current_sec = time.time()
|
||||||
year, month, day, hour, minute, second, weekday, _ = time.localtime(local_epoch)
|
|
||||||
|
|
||||||
time_str = f"{hour:02d}:{minute:02d}:{second:02d}"
|
# 2. Gate everything behind the 1-second boundary
|
||||||
date_str = f"{DAYS[weekday]} {year}-{month:02d}-{day:02d}"
|
if current_sec != last_second:
|
||||||
|
last_second = current_sec
|
||||||
|
|
||||||
oled.fill(0)
|
# Unpack UTC once per second
|
||||||
# Center-align roughly for 8x8 font
|
utc_year, utc_month, utc_day, utc_hour, _, _, utc_weekday, _ = time.gmtime(current_sec)
|
||||||
oled.text(time_str, 32, 20)
|
|
||||||
oled.text(date_str, 12, 38)
|
|
||||||
oled.show()
|
|
||||||
|
|
||||||
time.sleep(0.5)
|
# Run DST check
|
||||||
|
dst_offset = 1 if is_dst(utc_year, utc_month, utc_day, utc_hour, utc_weekday) else 0
|
||||||
|
total_offset = Config.UTC_OFFSET + dst_offset
|
||||||
|
|
||||||
|
# Compute local wall-clock components
|
||||||
|
local_epoch = current_sec + (total_offset * 3600)
|
||||||
|
year, month, day, hour_24, minute, second, weekday, _ = time.localtime(local_epoch)
|
||||||
|
|
||||||
|
# --- String formatting and OLED rendering ---
|
||||||
|
sep = ":" if second % 2 == 0 else " "
|
||||||
|
|
||||||
|
if Config.USE_24_HOUR:
|
||||||
|
time_str = f"{hour_24:02d}{sep}{minute:02d}"
|
||||||
|
period = None
|
||||||
|
else:
|
||||||
|
hour_12 = hour_24 % 12 or 12
|
||||||
|
time_str = f"{hour_12}{sep}{minute:02d}"
|
||||||
|
period = "AM" if hour_24 < 12 else "PM"
|
||||||
|
|
||||||
|
oled.fill(0)
|
||||||
|
time_x = calculate_x_spacing(time_str, scale=2)
|
||||||
|
draw_scaled_text(oled, time_str, x=time_x, y=TIME_Y, scale=2)
|
||||||
|
|
||||||
|
if period:
|
||||||
|
oled.text(period, time_x + (len(time_str) * 16) + 4, TIME_Y + 8)
|
||||||
|
|
||||||
|
date_str = f"{DAYS[weekday]} {year}-{month:02d}-{day:02d}"
|
||||||
|
oled.text(date_str, calculate_x_spacing(date_str), 42)
|
||||||
|
|
||||||
|
oled.show()
|
||||||
|
|
||||||
|
# Fast poll to catch the next second boundary immediately
|
||||||
|
time.sleep_ms(25)
|
||||||
|
|||||||
Reference in New Issue
Block a user