Added smart alarm clock project code

This commit is contained in:
dherrada 2021-07-01 17:05:45 -04:00
parent caf810104d
commit b16aa9c508
5 changed files with 14554 additions and 0 deletions

BIN
Smart_Alarm_Clock/alarm.wav Normal file

Binary file not shown.

116
Smart_Alarm_Clock/button.py Normal file
View file

@ -0,0 +1,116 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
import ssl
import time
import board
import digitalio
import socketpool
import wifi
import adafruit_minimqtt.adafruit_minimqtt as MQTT
from adafruit_io.adafruit_io import IO_MQTT
led = digitalio.DigitalInOut(board.IO8)
led.direction = digitalio.Direction.OUTPUT
btn1 = digitalio.DigitalInOut(board.IO9)
btn1.direction = digitalio.Direction.INPUT
btn1.pull = digitalio.Pull.DOWN
ALARM = None
### WiFi ###
# Get wifi details and more from a secrets.py file
try:
from secrets import secrets
except ImportError:
print("WiFi secrets are kept in secrets.py, please add them there!")
raise
print("Connecting to %s" % secrets["ssid"])
wifi.radio.connect(secrets["ssid"], secrets["password"])
print("Connected to %s!" % secrets["ssid"])
# Define callback functions which will be called when certain events happen.
# pylint: disable=unused-argument
def connected(client):
# Connected function will be called when the client is connected to Adafruit IO.
# This is a good place to subscribe to feed changes. The client parameter
# passed to this function is the Adafruit IO MQTT client so you can make
# calls against it easily.
print("Connected to Adafruit IO!")
client.subscribe("alarm-clock.alarm")
def subscribe(client, userdata, topic, granted_qos):
# This method is called when the client subscribes to a new feed.
print("Subscribed to {0} with QOS level {1}".format(topic, granted_qos))
def unsubscribe(client, userdata, topic, pid):
# This method is called when the client unsubscribes from a feed.
print("Unsubscribed from {0} with PID {1}".format(topic, pid))
# pylint: disable=unused-argument
def disconnected(client):
# Disconnected function will be called when the client disconnects.
print("Disconnected from Adafruit IO!")
# pylint: disable=unused-argument
def message(client, feed_id, payload):
# Message function will be called when a subscribed feed has a new value.
# The feed_id parameter identifies the feed, and the payload parameter has
# the new value.
print("Feed {0} received new value: {1}".format(feed_id, payload))
def on_alarm(client, feed_id, payload):
global ALARM
print(payload)
ALARM = eval(payload)
# Create a socket pool
pool = socketpool.SocketPool(wifi.radio)
# Initialize a new MQTT Client object
mqtt_client = MQTT.MQTT(
broker="io.adafruit.com",
username=secrets["aio_username"],
password=secrets["aio_key"],
socket_pool=pool,
ssl_context=ssl.create_default_context(),
)
# Initialize an Adafruit IO MQTT Client
io = IO_MQTT(mqtt_client)
# Connect the callback methods defined above to Adafruit IO
io.on_connect = connected
io.on_disconnect = disconnected
io.on_subscribe = subscribe
io.on_unsubscribe = unsubscribe
io.on_message = message
io.add_feed_callback("alarm-clock.alarm", on_alarm)
# Connect to Adafruit IO
print("Connecting to Adafruit IO...")
io.connect()
io.get("alarm-clock.alarm")
LAST = 0
while True:
io.loop()
if ALARM and time.monotonic() - LAST >= 0.2:
led.value = not led.value
LAST = time.monotonic()
if btn1.value:
io.publish("alarm-clock.alarm", "False")
led.value = False
led.value = True
time.sleep(1)
led.value = False

449
Smart_Alarm_Clock/code.py Normal file
View file

@ -0,0 +1,449 @@
# SPDX-FileCopyrightText: 2021 Dylan Herrada for Adafruit Industries
# SPDX-License-Identifier: MIT
# General imports
import gc
import time
import math
import board
# Display imports
import displayio
from adafruit_display_text import label
from adafruit_display_shapes.circle import Circle
from adafruit_display_shapes.line import Line
import adafruit_datetime
from adafruit_bitmap_font import bitmap_font
import framebufferio
import sharpdisplay
# Rotary encoder imports
from adafruit_seesaw.seesaw import Seesaw
from adafruit_seesaw.digitalio import DigitalIO
from adafruit_seesaw.rotaryio import IncrementalEncoder
# LED imports
import pwmio
# Adafruit IO imports
import ssl
import socketpool
import wifi
import adafruit_minimqtt.adafruit_minimqtt as MQTT
# Audio imputs
import audiocore
import audiobusio
TIMES = {}
ENABLED = {}
ALARM = False
WAIT = 0
LAST = False
LAST_TIME = 0
WARM_PERCENT = 0.0
COOL_PERCENT = 0.0
UTC_OFFSET = -4
days = {
0: "monday",
1: "tuesday",
2: "wednesday",
3: "thursday",
4: "friday",
5: "saturday",
6: "sunday",
}
try:
from secrets import secrets
except ImportError:
print("WiFi secrets are kept in secrets.py, please add them there!")
raise
# Set your Adafruit IO Username and Key in secrets.py
# (visit io.adafruit.com if you need to create an account,
# or if you need your Adafruit IO key.)
aio_username = secrets["aio_username"]
aio_key = secrets["aio_key"]
print("Connecting to %s" % secrets["ssid"])
wifi.radio.connect(secrets["ssid"], secrets["password"])
print("Connected to %s!" % secrets["ssid"])
# Define callback functions which will be called when certain events happen.
# pylint: disable=unused-argument
def connected(client, userdata, flags, rc):
print("Connected to Adafruit IO!")
feeds = [
"monday",
"monday-enable",
"tuesday",
"tuesday-enable",
"wednesday",
"wednesday-enable",
"thursday",
"thursday-enable",
"friday",
"friday-enable",
"saturday",
"saturday-enable",
"sunday",
"sunday-enable",
"alarm",
]
for feed in feeds:
feed_slug = secrets["aio_username"] + "/feeds/alarm-clock." + feed
print("Subscribed to: alarm-clock." + feed)
client.subscribe(feed_slug, 1)
def disconnected(client, userdata, rc): # pylint: disable=unused-argument
# Disconnected function will be called when the client disconnects.
print("Disconnected from Adafruit IO!")
displayio.release_displays()
def fade(warm_val, cool_val):
finished = False
if warm_val < 100 and cool_val == 0:
warm_val += 1
warm.duty_cycle = duty_cycle(warm_val)
elif warm_val == 100 and cool_val < 100:
cool_val += 1
cool.duty_cycle = duty_cycle(cool_val)
elif cool_val == 100 and warm_val > 0:
warm_val -= 1
warm.duty_cycle = duty_cycle(warm_val)
finished = True
return warm_val, cool_val, finished
def get(feed_key):
mqtt_client.publish(f'{secrets["aio_username"]}/feeds/{feed_key}/get', "\0")
time.sleep(0.1)
def on_iso(client, feed_id, payload):
timezone = adafruit_datetime.timezone.utc
timezone._offset = adafruit_datetime.timedelta(seconds=UTC_OFFSET * 3600)
datetime = adafruit_datetime.datetime.fromisoformat(payload[:-1]).replace(
tzinfo=timezone
)
local_datetime = datetime.tzinfo.fromutc(datetime)
print(local_datetime)
dt_hour = local_datetime.hour
dt_minute = local_datetime.minute
if not local_datetime.second % 10:
theta = (dt_hour / 6 + dt_minute / 360) - 0.5
y_1 = int((72 * math.sin(math.pi * theta)) + 128)
x_1 = int((72 * math.cos(math.pi * theta)) + 114)
new_hour = Line(114, 128, x_1, y_1, 0xFFFFFF)
splash[-3] = new_hour
theta = (dt_minute / 30) - 0.5
y_1 = int((96 * math.sin(math.pi * theta)) + 128)
x_1 = int((96 * math.cos(math.pi * theta)) + 114)
dt_minute = Line(114, 128, x_1, y_1, 0xFFFFFF)
splash[-2] = dt_minute
theta = (local_datetime.second / 30) - 0.5
y_1 = int((96 * math.sin(math.pi * theta)) + 128)
x_1 = int((96 * math.cos(math.pi * theta)) + 114)
new_second = Line(114, 128, x_1, y_1, 0x808080)
splash[-1] = new_second
day = days[local_datetime.weekday()]
alarm_hour, alarm_minute = TIMES[day].split(":")
if dt_hour == int(alarm_hour):
if (
dt_minute == int(alarm_minute)
and ENABLED[day]
and not ALARM
and WAIT < time.monotonic()
):
mqtt_client.publish(
f"{secrets['aio_username']}/feeds/alarm-clock.alarm", "True"
)
get("alarm-clock.alarm")
gc.collect()
def on_alarm(client, feed_id, payload):
global ALARM
global WAIT
global LAST
ALARM = eval(payload)
if ALARM is False and LAST is True:
WAIT = time.monotonic() + 60
LAST = ALARM
print(f"{feed_id}: {payload}")
def on_time(client, feed_id, payload):
global TIMES
TIMES[feed_id.split(".")[-1]] = payload
print(f"{feed_id}: {payload}")
def on_enable(client, feed_id, payload):
global ENABLED
ENABLED[feed_id.split(".")[-1].split("-")[0]] = payload == "ON"
print(f"{feed_id}: {payload}")
# Set up rotary encoder
i2c_bus = board.I2C()
seesaw = Seesaw(i2c_bus, addr=0x36)
seesaw_product = (seesaw.get_version() >> 16) & 0xFFFF
print("Found product {}".format(seesaw_product))
if seesaw_product != 4991:
print("Wrong firmware loaded? Expected 4991")
button = DigitalIO(seesaw, 24)
encoder = IncrementalEncoder(seesaw)
LAST_POSITION = 0
# Set up display
bus = board.SPI()
chip_select_pin = board.IO12
framebuffer = sharpdisplay.SharpMemoryFramebuffer(bus, chip_select_pin, 400, 240)
display = framebufferio.FramebufferDisplay(framebuffer)
splash = displayio.Group(max_size=13)
display.show(splash)
# Set up PWM LEDs
WARM_PIN = board.IO5
COOL_PIN = board.IO6
FADE_SLEEP = 0.01 # Number of milliseconds to delay between changes.
# Increase to slow down, decrease to speed up.
# Define PWM outputs:
warm = pwmio.PWMOut(WARM_PIN)
cool = pwmio.PWMOut(COOL_PIN)
def duty_cycle(percent):
return int(percent / 100.0 * 65535.0)
# Set up UI
def brighten_warm(i):
global WARM_PERCENT
WARM_PERCENT = max(min(WARM_PERCENT + float(i * 2), 100), 0)
warm.duty_cycle = duty_cycle(WARM_PERCENT)
def brighten_cool(i):
global COOL_PERCENT
COOL_PERCENT = max(min(COOL_PERCENT + float(i * 2), 100), 0)
cool.duty_cycle = duty_cycle(COOL_PERCENT)
menu_funcs = [brighten_warm, brighten_cool]
font = bitmap_font.load_font("/fonts/LeagueGothic-Regular-36.bdf")
label1 = label.Label(
font, text="Warm light", anchor_point=(0, 0.5), anchored_position=(235, 120)
)
splash.append(label1)
label2 = label.Label(
font, text="Cool light", anchor_point=(0, 0.5), anchored_position=(235, 170)
)
splash.append(label2)
menu = [label1, label2]
# Set up clock
circle = Circle(114, 128, 96, outline=0xFFFFFF)
splash.append(circle)
circle = Circle(114, 128, 3, outline=0xFFFFFF, fill=0xFFFFFF)
splash.append(circle)
twelve = Line(114, 32, 114, 16, 0xFFFFFF)
splash.append(twelve)
for i in range(-2, 9):
y0 = int((96 * math.sin(math.pi * (i / 6))) + 128)
x0 = int((96 * math.cos(math.pi * (i / 6))) + 114)
y1 = int((108 * math.sin(math.pi * (i / 6))) + 128)
x1 = int((108 * math.cos(math.pi * (i / 6))) + 114)
hour = Line(x0, y0, x1, y1, 0xFFFFFF)
splash.append(hour)
hour = Line(114, 128, 114, 128, 0xFFFFFF)
splash.append(hour)
minute = Line(114, 128, 114, 128, 0xFFFFFF)
splash.append(minute)
second = Line(114, 128, 114, 128, 0x808080)
splash.append(second)
time.sleep(1)
# Audio setup
wave_file = open("alarm.wav", "rb")
wave = audiocore.WaveFile(wave_file)
audio = audiobusio.I2SOut(board.IO8, board.IO14, board.IO13)
# Create a socket pool
pool = socketpool.SocketPool(wifi.radio)
# Initialize a new MQTT Client object
mqtt_client = MQTT.MQTT(
broker="io.adafruit.com",
username=secrets["aio_username"],
password=secrets["aio_key"],
socket_pool=pool,
ssl_context=ssl.create_default_context(),
)
# Connect the callback methods defined above to Adafruit IO
mqtt_client.on_connect = connected
mqtt_client.on_disconnect = disconnected
# Connect to Adafruit IO
mqtt_client.connect()
mqtt_client.add_topic_callback("time/ISO-8601", on_iso)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.alarm", on_alarm
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.sunday", on_time
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.monday", on_time
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.tuesday", on_time
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.wednesday", on_time
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.thursday", on_time
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.friday", on_time
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.saturday", on_time
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.sunday-enable", on_enable
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.monday-enable", on_enable
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.tuesday-enable", on_enable
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.wednesday-enable", on_enable
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.thursday-enable", on_enable
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.friday-enable", on_enable
)
mqtt_client.add_topic_callback(
secrets["aio_username"] + "/feeds/alarm-clock.saturday-enable", on_enable
)
get("alarm-clock.sunday")
get("alarm-clock.monday")
get("alarm-clock.tuesday")
get("alarm-clock.wednesday")
get("alarm-clock.thursday")
get("alarm-clock.friday")
get("alarm-clock.saturday")
get("alarm-clock.sunday-enable")
get("alarm-clock.monday-enable")
get("alarm-clock.tuesday-enable")
get("alarm-clock.wednesday-enable")
get("alarm-clock.thursday-enable")
get("alarm-clock.friday-enable")
get("alarm-clock.saturday-enable")
while len(TIMES) < 7 or len(ENABLED) < 7:
mqtt_client.loop()
mqtt_client.publish(secrets["aio_username"] + "/feeds/alarm-clock.alarm", "False")
mqtt_client.subscribe("time/ISO-8601", 1)
print("Starting")
SELECTED = 0
while True:
try:
diff = time.monotonic() - LAST_TIME
LAST_TIME = time.monotonic()
if diff > 0.1:
mqtt_client.loop(0.25)
if ALARM:
if WAIT > time.monotonic():
print("Alarm ringing")
WARM_PERCENT, COOL_PERCENT, done = fade(WARM_PERCENT, COOL_PERCENT)
if done and not audio.playing:
audio.play(wave)
position = encoder.position
if position < LAST_POSITION and button.value:
SELECTED -= 1
LAST_POSITION = position
print("Position: {}".format(position))
for i in menu:
i.y += 50
if position > LAST_POSITION and button.value:
SELECTED += 1
LAST_POSITION = position
for i in menu:
i.y -= 50
if not button.value:
if LAST_POSITION != position:
menu_funcs[SELECTED](position - LAST_POSITION)
LAST_POSITION = position
else:
pass
gc.collect()
except (ValueError, RuntimeError) as err:
print("Failed to get data, retrying\n", err)
wifi.reset()
mqtt_client.reconnect()
continue
except MemoryError:
print("Buffer too large, retrying")
continue

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,111 @@
Copyright (c) 2020, Caleb Maclennan <caleb@alerque>
Copyright (c) 2010-2012, Micah Rich <micah@micahrich.com>,
with Reserved Font Name: "League Gothic".
Copyright (c) 2010, Caroline Hadilaksono <caroline@hadilaksono>
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
Version 1.1 - 26 February 2007
----
SIL Open Font License
=====================
Preamble
--------
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
Definitions
-----------
`"Font Software"` refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
`"Reserved Font Name"` refers to any names specified as such after the
copyright statement(s).
`"Original Version"` refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
`"Modified Version"` refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
`"Author"` refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
Permission & Conditions
-----------------------
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1. Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2. Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3. No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4. The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5. The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
Termination
-----------
This license becomes null and void if any of the above conditions are
not met.
Disclaimer
----------
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.