Binary Clock / Weather Monitor
YouTube Video
Introduction
Welcome to this tutorial on creating a Binary Clock and Weather monitor using the Raspberry Pi Pico W. This tutorial is broken into two parts. The first part will focus on building a prototype on breadboard and second part will focus on creating a custom PCB with a 3D printed enclosure. The project will use the openweater.org API and worldtime clock API to collect the time and weather data.
Components - Prototype
The following is the list of components needed:
Component | Quantity |
---|---|
Raspberry Pi Pico W | 1 |
Micro USB Cable | 1 |
Breadboard | 1 |
Wires | Several |
Resistor | 8 (330Ω) |
Resistor | 5 (1KΩ) |
LED Red | 8 |
LED Blue | 5 |
OLED1366 | 1 |
Schematic Diagram
The Fritzing diagram is shown below, since we have limited space on the breadboard we will just display hours and minutes in binary.
An important note when making the connections check the pins on the OLED display as some modules the GND and VCC pins are changed.
Code
You are going to need 4 files for this project
- config.json: In this file we will store all our private information, Wifi-Password, API Key and our city and country code.
- ssd1306.py: The library for controlling the ssd1306 OLED display
- urequests.py: Connecting the Pico W to the internet to use API to collect data
- main.py: The main program that will run on boot when the Pico is powered.
You can download the code using the following link, or copy it from this webpage below.
json
{
"ssid": "Open_Internet",
"ssid_password": "25802580",
"query_interval_sec": 120,
"weather_api_key": "ce54c4b03cfc0bd0fc037188def2d98e",
"city": "Qingdao"
"country_code": "CN"
}
ssd1306.py
# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
from micropython import const
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_IREF_SELECT = const(0xAD)
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)
# Subclassing FrameBuffer provides support for graphics primitives
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
class SSD1306(framebuf.FrameBuffer):
def __init__(self, width, height, external_vcc):
self.width = width
self.height = height
self.external_vcc = external_vcc
self.pages = self.height // 8
self.buffer = bytearray(self.pages * self.width)
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
self.init_display()
def init_display(self):
for cmd in (
SET_DISP, # display off
# address setting
SET_MEM_ADDR,
0x00, # horizontal
# resolution and layout
SET_DISP_START_LINE, # start at line 0
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.width > 2 * self.height 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
SET_IREF_SELECT,
0x30, # enable internal IREF during display on
# charge pump
SET_CHARGE_PUMP,
0x10 if self.external_vcc else 0x14,
SET_DISP | 0x01, # display on
): # on
self.write_cmd(cmd)
self.fill(0)
self.show()
def poweroff(self):
self.write_cmd(SET_DISP)
def poweron(self):
self.write_cmd(SET_DISP | 0x01)
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 rotate(self, rotate):
self.write_cmd(SET_COM_OUT_DIR | ((rotate & 1) << 3))
self.write_cmd(SET_SEG_REMAP | (rotate & 1))
def show(self):
x0 = 0
x1 = self.width - 1
if self.width != 128:
# narrow displays use centred columns
col_offset = (128 - self.width) // 2
x0 += col_offset
x1 += col_offset
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_data(self.buffer)
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)
self.write_list = [b"\x40", None] # Co=0, D/C#=1
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_data(self, buf):
self.write_list[1] = buf
self.i2c.writevto(self.addr, self.write_list)
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
import time
self.res(1)
time.sleep_ms(1)
self.res(0)
time.sleep_ms(10)
self.res(1)
super().__init__(width, height, external_vcc)
def write_cmd(self, cmd):
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
self.cs(1)
self.dc(0)
self.cs(0)
self.spi.write(bytearray([cmd]))
self.cs(1)
def write_data(self, buf):
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
self.cs(1)
self.dc(1)
self.cs(0)
self.spi.write(buf)
self.cs(1)
urequests.py
import usocket
class Response:
def __init__(self, f):
self.raw = f
self.encoding = "utf-8"
self._cached = None
def close(self):
if self.raw:
self.raw.close()
self.raw = None
self._cached = None
@property
def content(self):
if self._cached is None:
try:
self._cached = self.raw.read()
finally:
self.raw.close()
self.raw = None
return self._cached
@property
def text(self):
return str(self.content, self.encoding)
def json(self):
import ujson
return ujson.loads(self.content)
def request(method, url, data=None, json=None, headers={}, stream=None):
try:
proto, dummy, host, path = url.split("/", 3)
except ValueError:
proto, dummy, host = url.split("/", 2)
path = ""
if proto == "http:":
port = 80
elif proto == "https:":
import ussl
port = 443
else:
raise ValueError("Unsupported protocol: " + proto)
if ":" in host:
host, port = host.split(":", 1)
port = int(port)
ai = usocket.getaddrinfo(host, port, 0, usocket.SOCK_STREAM)
try:
ai = ai[0]
except:
print("Count not resolve getaddrinfo for {} {}".format(host,port))
s = usocket.socket(ai[0], ai[1], ai[2])
try:
s.connect(ai[-1])
if proto == "https:":
s = ussl.wrap_socket(s, server_hostname=host)
s.write(b"%s /%s HTTP/1.0\r\n" % (method, path))
if not "Host" in headers:
s.write(b"Host: %s\r\n" % host)
# Iterate over keys to avoid tuple alloc
for k in headers:
s.write(k)
s.write(b": ")
s.write(headers[k])
s.write(b"\r\n")
if json is not None:
assert data is None
import ujson
data = ujson.dumps(json)
s.write(b"Content-Type: application/json\r\n")
if data:
s.write(b"Content-Length: %d\r\n" % len(data))
s.write(b"\r\n")
if data:
s.write(data)
l = s.readline()
#print(l)
l = l.split(None, 2)
status = int(l[1])
reason = ""
if len(l) > 2:
reason = l[2].rstrip()
while True:
l = s.readline()
if not l or l == b"\r\n":
break
#print(l)
if l.startswith(b"Transfer-Encoding:"):
if b"chunked" in l:
raise ValueError("Unsupported " + l)
elif l.startswith(b"Location:") and not 200 <= status <= 299:
raise NotImplementedError("Redirects not yet supported")
except OSError:
s.close()
raise
resp = Response(s)
resp.status_code = status
resp.reason = reason
return resp
def head(url, **kw):
return request("HEAD", url, **kw)
def get(url, **kw):
return request("GET", url, **kw)
def post(url, **kw):
return request("POST", url, **kw)
def put(url, **kw):
return request("PUT", url, **kw)
def patch(url, **kw):
return request("PATCH", url, **kw)
def delete(url, **kw):
return request("DELETE", url, **kw)
main.py
import utime
from machine import Pin, I2C, RTC
from ssd1306 import SSD1306_I2C
import urequests
import network
import json
import time
# Load configuration
with open('config.json') as f:
config = json.load(f)
# Check config.json has updated credentials
if config['ssid'] == 'Enter_Wifi_SSID':
assert False, ("config.json has not been updated with your unique keys and data")
# Your OpenWeatherMap API details
weather_api_key = config['weather_api_key']
city = config['city']
country_code = config['country_code']
# Create WiFi connection and turn it on
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
# Connect to WiFi router
print("Connecting to WiFi: {}".format(config['ssid']))
wlan.connect(config['ssid'], config['ssid_password'])
# Wait until WiFi is connected
while not wlan.isconnected():
utime.sleep(1)
print("Connected to Wi-Fi:", wlan.ifconfig())
# Function to sync time with worldtimeapi.org
def sync_time_with_worldtimeapi_org(rtc, blocking=True):
TIME_API = "http://worldtimeapi.org/api/timezone/Asia/Shanghai"
response = None
while True:
try:
response = urequests.get(TIME_API)
break
except:
if blocking:
response.close()
continue
else:
response.close()
return
json_data = response.json()
current_time = json_data["datetime"]
the_date, the_time = current_time.split("T")
year, month, mday = [int(x) for x in the_date.split("-")]
the_time = the_time.split(".")[0]
hours, minutes, seconds = [int(x) for x in the_time.split(":")]
week_day = json_data["day_of_week"]
response.close()
rtc.datetime((year, month, mday, week_day, hours, minutes, seconds, 0))
# Function to fetch weather data
def fetch_weather():
open_weather_map_url = f"http://api.openweathermap.org/data/2.5/weather?q={city},{country_code}&appid={weather_api_key}&units=metric"
try:
print("Fetching weather data from:", open_weather_map_url)
weather_data = urequests.get(open_weather_map_url)
if weather_data.status_code == 200:
weather_json = weather_data.json()
print("Weather Data:", weather_json)
# Extracting relevant weather information
return {
'location': weather_json.get('name') + ' - ' + weather_json.get('sys').get('country'),
'description': weather_json.get('weather')[0].get('main'),
'temperature': weather_json.get('main').get('temp'),
'pressure': weather_json.get('main').get('pressure'),
'humidity': weather_json.get('main').get('humidity'),
'wind_speed': weather_json.get('wind').get('speed'),
}
else:
print("Weather API Error:", weather_data.status_code, weather_data.text)
except Exception as e:
print("An error occurred while fetching weather data:", str(e))
return None
rtc = RTC()
sync_time_with_worldtimeapi_org(rtc)
# Define the GPIO pins for the LEDs
hour_pins = [Pin(pin, Pin.OUT) for pin in [1, 0]] # 2 bits for hours (0-1)
hour_pins_ext = [Pin(pin, Pin.OUT) for pin in [11,4,3,2]] # 4 bits for hours (2-5)
minute_pins = [Pin(pin, Pin.OUT) for pin in [8,7,6]] # 3 bits for first minute (0-7)
minute_pins_ext = [Pin(pin, Pin.OUT) for pin in [21,20,10,9]] # 4 bits for second minute (0-9)
second_pins = [Pin(pin, Pin.OUT) for pin in [15,14,13]] # 3 bits for first second (0-7)
second_pins_ext = [Pin(pin, Pin.OUT) for pin in [12 , 5 , 19,18]] # 4 bits for second second (0-9)
# Function to update the LEDs based on the current time
def update_leds():
Y, M, D, W, H, Min, S, SS = rtc.datetime() # Changed M to Min to avoid conflict
print("Time:", H, ":", Min, ":", S)
hour_msb = H // 10
hour_lsb = H % 10
minute_msb = Min // 10
minute_lsb = Min % 10
second_msb = S // 10
second_lsb = S % 10
hour_msb_binary = '{0:02b}'.format(hour_msb)
hour_lsb_binary = '{0:04b}'.format(hour_lsb)
hour_pins[0].value(int(hour_msb_binary[0]))
hour_pins[1].value(int(hour_msb_binary[1]))
for i in range(4):
hour_pins_ext[i].value(int(hour_lsb_binary[i]))
minute_msb_binary = '{0:03b}'.format(minute_msb)
minute_lsb_binary = '{0:04b}'.format(minute_lsb)
for i in range(3):
minute_pins[i].value(int(minute_msb_binary[i]))
for i in range(4):
minute_pins_ext[i].value(int(minute_lsb_binary[i]))
second_msb_binary = '{0:03b}'.format(second_msb)
second_lsb_binary = '{0:04b}'.format(second_lsb)
for i in range(3):
second_pins[i].value(int(second_msb_binary[i]))
for i in range(4):
second_pins_ext[i].value(int(second_lsb_binary[i]))
# OLED display dimensions
WIDTH = 128
HEIGHT = 64
# Initialize I2C and OLED display
i2c = I2C(0, scl=Pin(17), sda=Pin(16), freq=400000)
display = SSD1306_I2C(WIDTH, HEIGHT, i2c)
# Function to update the OLED display with weather data
def update_display(weather_data):
display.fill(0) # Clear the display
display.text('{}'.format(weather_data['location']), 0, 0)
display.text('Temp: {} C'.format(weather_data['temperature']), 0, 10)
display.text('Desc: {}'.format(weather_data['description']), 0, 20)
display.show() # Update the display
# Fetch initial weather data
weather_data = fetch_weather()
if weather_data:
update_display(weather_data)
# Loop indefinitely, updating the LEDs every second and checking for weather updates every 10 minutes
last_weather_update = utime.time()
while True:
update_leds()
# Check if 10 minutes have passed
if utime.time() - last_weather_update > 600: # 600 seconds = 10 minutes
weather_data = fetch_weather()
if weather_data:
update_display(weather_data) # Update the display with new weather data
last_weather_update = utime.time() # Reset the timer
time.sleep(1)
Code Overview:
This project combines a binary clock and a weather monitor, displaying the current time and weather information on an OLED display. Below is a breakdown of the main components of the code.
Configuration Loading
The program starts by loading configuration settings from a config.json file, which contains necessary information like Wi-Fi credentials and API keys.
# Load configuration
with open('config.json') as f:
config = json.load(f)
# Check if configuration has been updated
if config['ssid'] == 'Enter_Wifi_SSID':
assert False, ("config.json has not been updated with your unique keys and data")
Wi-Fi Connection
Next, the code establishes a connection to the specified Wi-Fi network. It continuously checks until the connection is successful.
# Create WiFi connection and turn it on
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
# Connect to WiFi router
print("Connecting to WiFi: {}".format(config['ssid']))
wlan.connect(config['ssid'], config['ssid_password'])
# Wait until WiFi is connected
while not wlan.isconnected():
utime.sleep(1)
print("Connected to Wi-Fi:", wlan.ifconfig())
Time Synchronization
The system synchronizes the real-time clock (RTC) with the current time from the worldtimeapi.org API, ensuring accurate timekeeping.
def sync_time_with_worldtimeapi_org(rtc):
TIME_API = "http://worldtimeapi.org/api/timezone/Asia/Shanghai"
response = urequests.get(TIME_API)
json_data = response.json()
current_time = json_data["datetime"]
# Parse and update RTC
...
rtc.datetime((year, month, mday, week_day, hours, minutes, seconds, 0))
Fetching Weather Data
The program fetches weather data from the OpenWeatherMap API using the specified city and country code. It handles errors and extracts relevant weather information.
def fetch_weather():
open_weather_map_url = f"http://api.openweathermap.org/data/2.5/weather?q={city},{country_code}&appid={weather_api_key}&units=metric"
weather_data = urequests.get(open_weather_map_url)
if weather_data.status_code == 200:
weather_json = weather_data.json()
return {
'location': weather_json.get('name') + ' - ' + weather_json.get('sys').get('country'),
'description': weather_json.get('weather')[0].get('main'),
'temperature': weather_json.get('main').get('temp'),
...
}
else:
print("Weather API Error:", weather_data.status_code, weather_data.text)
return None
LED Control
The code defines GPIO pins to control LEDs, which represent the current time in binary format. The current time is converted into binary and updated accordingly.
def update_leds():
Y, M, D, W, H, Min, S, SS = rtc.datetime()
hour_msb = H // 10
hour_lsb = H % 10
...
hour_pins[0].value(int(hour_msb_binary[0]))
hour_pins[1].value(int(hour_msb_binary[1]))
...
OLED Display
An OLED display is initialized to show the weather information. It is updated with the latest data retrieved from the API.
def update_display(weather_data):
display.fill(0) # Clear the display
display.text('{}'.format(weather_data['location']), 0, 0)
display.text('Temp: {} C'.format(weather_data['temperature']), 0, 10)
...
display.show() # Update the display
Main Loop
Finally, the main loop continuously updates the LEDs and checks for new weather data every 10 minutes. It also updates the OLED display to reflect the current weather.
while True:
update_leds()
# Check if 10 minutes have passed
if utime.time() - last_weather_update > 600:
weather_data = fetch_weather()
if weather_data:
update_display(weather_data) # Update the display with new weather data
last_weather_update = utime.time() # Reset the timer
time.sleep(1)
This binary clock and weather monitor project showcases how to integrate various components to create a functional and visually appealing device. If you have any questions let me know on discord.
PCB Design
The PCB was designed using EasyEDA, a free and user-friendly web-based tool that supports circuit design, simulation, and PCB layout. I like the look of having the electronics exposed, so I have placed the resistors on the top face of the PCB.
This design includes several key features:
- Two Push Buttons: These two push buttons will be used to display different data on the OLED display
- Mounting Holes: The PCB is equipped with four 3mm mounting holes, making it easy to secure within an enclosure.
- Power input Terminal: This will allow to connect 5V through DC plug or calbe depending on your needs.
PCB Top:
Order PCB (JLCPCB)
The PCB was ordered through JLCPCB. They offer great PCBs at a low cost and have promotions and coupons available throughout the year. You can sign up using here, or using the following link:
https://jlcpcb.com/?from=Nerd that will support me as a creator to keep making content that is accessible and open source at no charge to you.
Ordering the PCB is very simple:
Download the Gerber file here.
Click on Add Gerber file
leave all the settings as default given. You might want change the PCB color which you can do here:
Enter you shipping details, save to cart
Then after a few days depending on your location you will receive your great quality PCB.
Code
The following code demonstrates how to control WS2812B LED strips using the Neopixel library on the Raspberry Pi Pico. This example defines several colors and initializes multiple LED strips with the ability to control their colors.
Conclusion:
In this tutorial, we’ve explored how to create a custom PCB controller for WS2812B LEDs using either a Raspberry Pi Pico or a D1 Mini. We walked through the entire process, from designing the PCB and ordering it through JLCPCB to setting up the Raspberry Pi Pico and D1 Mini with WLED firmware.
What We’ve Learned
- PCB Design: How to design a PCB using EasyEDA, incorporating features such as push buttons and Bluetooth connectivity.
- Firmware Installation: How to flash WLED onto a D1 Mini to enable easy, wireless control of your LED strips.
- LED Control: Basic programming with the Neopixel library on the Raspberry Pi Pico and WLED setup for intuitive LED management.
Benefits of the Project
- Custom Control: With this setup, you gain precise control over your LED strips, allowing for a wide range of colors and effects.
- Flexibility: By using either the Raspberry Pi Pico or the D1 Mini, you can choose the hardware that best fits your needs and preferences.
- Enhanced Creativity: This project provides a solid foundation for various creative applications, from ambient lighting to interactive displays.
I encourage you to experiment with the design, modify the code, and explore the many possibilities that WLED and Neopixel control offer. Share your results, projects, or any modifications you make with our community. Your feedback and creativity help inspire others and contribute to the ongoing development of open-source projects.
Thank you for following along with this tutorial. I hope you enjoyed the process and are excited to apply what you’ve learned to your own projects. Happy building!