Merge pull request #1 from mgrove36/modular-screens
Modularise screens and improve button actions
This commit is contained in:
102
api.py
102
api.py
@@ -1,84 +1,94 @@
|
|||||||
from requests import get, post
|
from requests import get, post
|
||||||
import gc
|
from gc import collect
|
||||||
import time
|
from time import mktime, localtime
|
||||||
|
from env import HASS_URL, TOKEN
|
||||||
|
|
||||||
class Api:
|
def getReq(endpoint) -> dict:
|
||||||
def __init__(self, base_url, access_token) -> None:
|
collect()
|
||||||
self.base_url = base_url
|
|
||||||
self.access_token = access_token
|
|
||||||
|
|
||||||
def __request(self, endpoint) -> dict:
|
|
||||||
gc.collect()
|
|
||||||
url = self.base_url + endpoint
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer " + self.access_token,
|
"Authorization": "Bearer " + TOKEN,
|
||||||
"content-type": "application/json"
|
"content-type": "application/json"
|
||||||
}
|
}
|
||||||
print("Starting request for " + endpoint)
|
print("Starting request for " + endpoint)
|
||||||
response = get(url, headers=headers)
|
try:
|
||||||
|
response = get(HASS_URL + endpoint, headers=headers)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
except:
|
||||||
|
return {}
|
||||||
|
|
||||||
def __post(self, endpoint, d) -> bool:
|
def postReq(endpoint, d) -> bool:
|
||||||
gc.collect()
|
collect()
|
||||||
url = self.base_url + endpoint
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": "Bearer " + self.access_token,
|
"Authorization": "Bearer " + TOKEN,
|
||||||
"content-type": "application/json"
|
"content-type": "application/json"
|
||||||
}
|
}
|
||||||
print("Starting post request to " + endpoint)
|
print("Starting post request to " + endpoint)
|
||||||
response = post(url, headers=headers, json=d)
|
try:
|
||||||
|
response = post(HASS_URL + endpoint, headers=headers, json=d)
|
||||||
return response.status_code == 200
|
return response.status_code == 200
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
def getLightData(self, entity_id: str) -> dict:
|
def getLightData(entity_id: str) -> dict:
|
||||||
# TODO: error handling if can't access hass (e.g. no connection, invalid token)
|
# TODO: error handling if can't access hass (e.g. no connection, invalid token)
|
||||||
response = self.__request("/api/states/" + entity_id)
|
response = getReq("/api/states/" + entity_id)
|
||||||
on = response["state"] == "on"
|
on = "state" in response and response["state"] == "on"
|
||||||
return {
|
return {
|
||||||
"on": on,
|
"on": on,
|
||||||
"rgb_color": response["attributes"]["rgb_color"] if on else None,
|
"rgb_color": response["attributes"]["rgb_color"] if on else None,
|
||||||
"rgbw_color": response["attributes"]["rgbw_color"] if on else None,
|
"rgbw_color": response["attributes"]["rgbw_color"] if on else None,
|
||||||
"brightness": response["attributes"]["brightness"] if on else 0,
|
"brightness": response["attributes"]["brightness"] if on else 0.0
|
||||||
"friendly_name": response["attributes"]["friendly_name"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def getMediaPlayerData(self, entity_id: str) -> dict:
|
def getMediaPlayerData(entity_id: str) -> dict:
|
||||||
response = self.__request("/api/states/" + entity_id)
|
response = getReq("/api/states/" + entity_id)
|
||||||
|
if (not "state" in response or not "attributes" in response):
|
||||||
|
response = {
|
||||||
|
"state": "",
|
||||||
|
"attributes": {}
|
||||||
|
}
|
||||||
|
e = "state" in response and "attributes" in response
|
||||||
if ("media_position" in (dict)(response["attributes"])):
|
if ("media_position" in (dict)(response["attributes"])):
|
||||||
p = response["attributes"]["media_position"]
|
p = response["attributes"]["media_position"]
|
||||||
else:
|
else:
|
||||||
p = 0
|
p = 0
|
||||||
if ("media_position_updated_at" in (dict)(response["attributes"])):
|
if ("media_position_updated_at" in (dict)(response["attributes"])):
|
||||||
dts = response["attributes"]["media_position_updated_at"]
|
dts = response["attributes"]["media_position_updated_at"]
|
||||||
t = time.mktime((int(dts[0:4]), int(dts[5:7]), int(dts[8:10]), int(dts[11:13]) + int(dts[27:29]), int(dts[14:16]) + int(dts[30:31]), int(dts[17:19]), 0, 0))
|
t = mktime((int(dts[0:4]), int(dts[5:7]), int(dts[8:10]), int(dts[11:13]) + int(dts[27:29]), int(dts[14:16]) + int(dts[30:31]), int(dts[17:19]), 0, 0))
|
||||||
p += time.mktime(time.localtime()) - t
|
p += mktime(localtime()) - t
|
||||||
return {
|
return {
|
||||||
"playing": response["state"] in ["on", "playing", "buffering"],
|
"playing": response["state"] in ["on", "playing", "buffering"],
|
||||||
"shuffle": response["attributes"]["shuffle"],
|
"shuffle": response["attributes"]["shuffle"] if e and "shuffle" in response["attributes"] else False,
|
||||||
"repeat": response["attributes"]["repeat"] == "on",
|
"repeat": response["attributes"]["repeat"] == "on" if e and "repeat" in response["attributes"] else False,
|
||||||
"volume_level": response["attributes"]["volume_level"],
|
"volume_level": response["attributes"]["volume_level"] if e and "volume_level" in response["attributes"] else 0,
|
||||||
"friendly_name": response["attributes"]["friendly_name"],
|
"entity_picture": response["attributes"]["entity_picture"] if e and "entity_picture" in (dict)(response["attributes"]) else None,
|
||||||
"entity_picture": response["attributes"]["entity_picture"] if "entity_picture" in (dict)(response["attributes"]) else None,
|
"media_duration": response["attributes"]["media_duration"] if e and "media_duration" in (dict)(response["attributes"]) else 0,
|
||||||
"media_duration": response["attributes"]["media_duration"] if "media_duration" in (dict)(response["attributes"]) else 0,
|
|
||||||
"media_position": p,
|
"media_position": p,
|
||||||
"media_title": response["attributes"]["media_title"] if "media_title" in (dict)(response["attributes"]) else "Nothing playing",
|
"media_title": response["attributes"]["media_title"] if e and "media_title" in (dict)(response["attributes"]) else "Nothing playing",
|
||||||
"media_artist": response["attributes"]["media_artist"] if "media_artist" in (dict)(response["attributes"]) else "",
|
"media_artist": response["attributes"]["media_artist"] if e and "media_artist" in (dict)(response["attributes"]) else "",
|
||||||
"media_album_name": response["attributes"]["media_album_name"] if "media_album_name" in (dict)(response["attributes"]) else ""
|
"media_album_name": response["attributes"]["media_album_name"] if e and "media_album_name" in (dict)(response["attributes"]) else ""
|
||||||
}
|
}
|
||||||
|
|
||||||
def changeVolume(self, entity_id: str, up: bool = True) -> None:
|
def changeVolume(entity_id: str, up: bool = True) -> None:
|
||||||
if (up): dir = "up"
|
if (up): dir = "up"
|
||||||
else: dir = "down"
|
else: dir = "down"
|
||||||
self.__post(f"/api/services/media_player/volume_{dir}", {"entity_id": entity_id})
|
postReq(f"/api/services/media_player/volume_{dir}", {"entity_id": entity_id})
|
||||||
self.__post(f"/api/services/media_player/volume_{dir}", {"entity_id": entity_id})
|
postReq(f"/api/services/media_player/volume_{dir}", {"entity_id": entity_id})
|
||||||
|
|
||||||
def nextTrack(self, entity_id: str) -> None:
|
def setVolume(entity_id: str, v: float) -> None:
|
||||||
self.__post("/api/services/media_player/media_next_track", {"entity_id": entity_id})
|
postReq(f"/api/services/media_player/volume_set", {"entity_id": entity_id, "volume_level": v})
|
||||||
|
|
||||||
def prevTrack(self, entity_id: str) -> None:
|
def nextTrack(entity_id: str) -> None:
|
||||||
self.__post("/api/services/media_player/media_previous_track", {"entity_id": entity_id})
|
postReq("/api/services/media_player/media_next_track", {"entity_id": entity_id})
|
||||||
|
|
||||||
def playPause(self, entity_id: str) -> None:
|
def prevTrack(entity_id: str) -> None:
|
||||||
self.__post("/api/services/media_player/media_play_pause", {"entity_id": entity_id})
|
postReq("/api/services/media_player/media_previous_track", {"entity_id": entity_id})
|
||||||
|
|
||||||
def toggleLight(self, entity_id: str) -> None:
|
def playPause(entity_id: str) -> None:
|
||||||
self.__post("/api/services/light/toggle", {"entity_id": entity_id})
|
postReq("/api/services/media_player/media_play_pause", {"entity_id": entity_id})
|
||||||
|
|
||||||
|
def toggleLight(entity_id: str) -> None:
|
||||||
|
postReq("/api/services/light/toggle", {"entity_id": entity_id})
|
||||||
|
|
||||||
|
def setBrightness(entity_id: str, v: int) -> None:
|
||||||
|
postReq("/api/services/light/turn_on", {"entity_id": entity_id, "brightness": v})
|
||||||
|
|||||||
264
app.py
264
app.py
@@ -1,29 +1,24 @@
|
|||||||
from api import Api
|
from machine import reset
|
||||||
import machine
|
from network import WLAN, STA_IF
|
||||||
import network
|
from time import sleep, localtime
|
||||||
from time import sleep
|
from lcd import LCD
|
||||||
from screen import LCD
|
from font import cntr_st
|
||||||
from font import Font
|
from gc import collect, mem_free
|
||||||
import gc
|
from env import HOSTNAME, SSID, WIFI_PASSWORD, SCREENS
|
||||||
from env import *
|
from _thread import start_new_thread
|
||||||
from utils import Utils
|
from ntptime import settime
|
||||||
import _thread
|
from screens import *
|
||||||
import bmp_file_reader as bmpr
|
|
||||||
import ntptime
|
|
||||||
import time
|
|
||||||
|
|
||||||
class App:
|
class App:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
if (len(SCREENS) == 0):
|
if (len(SCREENS) == 0):
|
||||||
print("No screens configured. Check env.py.")
|
print("No screens configured. Check env.py.")
|
||||||
self.screen = -1
|
self.scr_n = -1
|
||||||
else: self.screen = 0
|
else: self.scr_n = 0
|
||||||
self.lcd = LCD()
|
self.lcd = LCD()
|
||||||
self.api = Api(HASS_URL, TOKEN)
|
|
||||||
self.scr_vals = {} # stored values from the current screen
|
|
||||||
|
|
||||||
def __connect(self) -> int:
|
def __connect(self) -> int:
|
||||||
wlan = network.WLAN(network.STA_IF)
|
wlan = WLAN(STA_IF)
|
||||||
wlan.active(True)
|
wlan.active(True)
|
||||||
wlan.config(hostname=HOSTNAME)
|
wlan.config(hostname=HOSTNAME)
|
||||||
wlan.connect(SSID, WIFI_PASSWORD)
|
wlan.connect(SSID, WIFI_PASSWORD)
|
||||||
@@ -35,14 +30,17 @@ class App:
|
|||||||
return self.ip
|
return self.ip
|
||||||
|
|
||||||
def __boot(self) -> None:
|
def __boot(self) -> None:
|
||||||
self.lcd.fill(self.lcd.black)
|
collect()
|
||||||
Font.cntr_st(self.lcd, self.lcd.width, "Booting...", 120, 2, 150, 150, 150)
|
print("Booting")
|
||||||
|
self.lcd.fill(0x0000)
|
||||||
|
cntr_st(self.lcd, self.lcd.width, "Booting...", 120, 2, 150, 150, 150)
|
||||||
self.lcd.show()
|
self.lcd.show()
|
||||||
self.__connect()
|
self.__connect()
|
||||||
print("Local time before synchronization:%s" %str(time.localtime()))
|
try:
|
||||||
ntptime.host = "1.europe.pool.ntp.org"
|
settime()
|
||||||
ntptime.settime()
|
print("Local time after synchronization: %s" %str(localtime()))
|
||||||
print("Local time after synchronization:%s" %str(time.localtime()))
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def __resetButtonStatuses(self) -> None:
|
def __resetButtonStatuses(self) -> None:
|
||||||
self.lcd.keyA["v"] = False
|
self.lcd.keyA["v"] = False
|
||||||
@@ -89,209 +87,75 @@ class App:
|
|||||||
sleep(0.2)
|
sleep(0.2)
|
||||||
|
|
||||||
def __changeScreen(self) -> bool:
|
def __changeScreen(self) -> bool:
|
||||||
orig = self.screen
|
c = False
|
||||||
if (self.lcd.right["v"]):
|
if (self.lcd.right["v"]):
|
||||||
if (self.screen == len(SCREENS) - 1):
|
c = True
|
||||||
self.screen = 0
|
if (self.scr_n == len(SCREENS) - 1):
|
||||||
|
self.scr_n = 0
|
||||||
else:
|
else:
|
||||||
self.screen += 1
|
self.scr_n += 1
|
||||||
elif (self.lcd.left["v"]):
|
elif (self.lcd.left["v"]):
|
||||||
if (self.screen == 0):
|
c = True
|
||||||
self.screen = len(SCREENS) - 1
|
if (self.scr_n == 0):
|
||||||
|
self.scr_n = len(SCREENS) - 1
|
||||||
else:
|
else:
|
||||||
self.screen -= 1
|
self.scr_n -= 1
|
||||||
change = self.screen != orig
|
|
||||||
if (change):
|
|
||||||
self.scr_vals = {}
|
|
||||||
self.lcd.left["v"] = False
|
self.lcd.left["v"] = False
|
||||||
self.lcd.right["v"] = False
|
self.lcd.right["v"] = False
|
||||||
print(f"Screen change: {change}")
|
print(f"Screen change: {c}")
|
||||||
return change
|
return c
|
||||||
|
|
||||||
def __displayLightEntity(self, i: int, w: int, h: int, xo: int, yo: int, n: str, d) -> None:
|
def handleButtons(self) -> bool:
|
||||||
self.scr_vals[i] = d
|
|
||||||
# if the light is turned on, display the filled-in lightbulb icon in the colour of the light, centrally in the light's grid square
|
|
||||||
if (d["on"]):
|
|
||||||
color = Utils.colour(d["rgb_color"][0], d["rgb_color"][1], d["rgb_color"][2])
|
|
||||||
with open("images/lightbulb-on.bmp", "rb") as file_handle:
|
|
||||||
reader = bmpr.BMPFileReader(file_handle)
|
|
||||||
img_height = reader.get_height()
|
|
||||||
x_offset = w//2 + xo - reader.get_width()//2
|
|
||||||
y_offset = h//2 + yo - reader.get_height()//2 - 4
|
|
||||||
for row_i in range(0, reader.get_height()):
|
|
||||||
row = reader.get_row(row_i)
|
|
||||||
for col_i, color in enumerate(row):
|
|
||||||
r = d["rgb_color"][0] if (color.red) != 0 else 0
|
|
||||||
g = d["rgb_color"][1] if (color.green) != 0 else 0
|
|
||||||
b = d["rgb_color"][2] if (color.blue) != 0 else 0
|
|
||||||
if (color.red != 0 or color.green != 0 or color.blue != 0):
|
|
||||||
self.lcd.pixel(col_i + x_offset, row_i + y_offset, Utils.colour(r,g,b))
|
|
||||||
# otherwise display the outline lightbulb icon in grey, centrally in the light's grid square
|
|
||||||
else:
|
|
||||||
color = Utils.colour(80, 80, 80)
|
|
||||||
with open("images/lightbulb-off.bmp", "rb") as file_handle:
|
|
||||||
reader = bmpr.BMPFileReader(file_handle)
|
|
||||||
img_height = reader.get_height()
|
|
||||||
x_offset = w//2 + xo - reader.get_width()//2
|
|
||||||
y_offset = h//2 + yo - reader.get_height()//2 - 4
|
|
||||||
for row_i in range(0, reader.get_height()):
|
|
||||||
row = reader.get_row(row_i)
|
|
||||||
for col_i, color in enumerate(row):
|
|
||||||
if (color.red != 0 or color.green != 0 or color.blue != 0):
|
|
||||||
self.lcd.pixel(col_i + x_offset, row_i + y_offset, Utils.colour(color.red, color.green, color.blue))
|
|
||||||
else:
|
|
||||||
self.lcd.pixel(col_i + x_offset, row_i + y_offset, Utils.colour(0,0,0))
|
|
||||||
# display the name of the light 8px below the lightbulb icon
|
|
||||||
Font.cntr_st(self.lcd, w, n, y_offset + img_height + 8, 2, 220, 220, 220, xo)
|
|
||||||
|
|
||||||
def __displayLightsScreen(self, d=None) -> None:
|
|
||||||
# display up to four lights as defined in env.py
|
|
||||||
self.lcd.fill(self.lcd.black)
|
|
||||||
Font.cntr_st(self.lcd, self.lcd.width, SCREENS[self.screen]["name"], 10, 2, 255, 255, 255)
|
|
||||||
entities = len(SCREENS[self.screen]["entities"])
|
|
||||||
# for each defined entity (up to a max total of 4), display its data in a 2x2 grid
|
|
||||||
if (d == None): d = []
|
|
||||||
for i in range(0, min(entities, 4)):
|
|
||||||
if (len(d) == i): d.append(self.api.getLightData(SCREENS[self.screen]["entities"][i]["id"]))
|
|
||||||
self.__displayLightEntity(i, self.lcd.width//2, self.lcd.height//2, self.lcd.width//2 * (i % 2), self.lcd.height//2 * (i//2), SCREENS[self.screen]["entities"][i]["name"], d[i])
|
|
||||||
self.lcd.show()
|
|
||||||
# TODO: handle buttons
|
|
||||||
|
|
||||||
def __updateMediaPositionBar(self, d):
|
|
||||||
if (d["media_position"] != None and d["media_duration"] != None and d["media_duration"] != 0):
|
|
||||||
for x in range (0, (self.lcd.width * d["media_position"])//d["media_duration"]):
|
|
||||||
self.lcd.pixel(x, self.lcd.height - 5, self.lcd.white)
|
|
||||||
self.lcd.pixel(x, self.lcd.height - 4, self.lcd.white)
|
|
||||||
self.lcd.pixel(x, self.lcd.height - 3, self.lcd.white)
|
|
||||||
self.lcd.pixel(x, self.lcd.height - 2, self.lcd.white)
|
|
||||||
self.lcd.pixel(x, self.lcd.height - 1, self.lcd.white)
|
|
||||||
self.lcd.pixel(x, self.lcd.height, self.lcd.white)
|
|
||||||
|
|
||||||
def __displayMediaScreen(self, d=None) -> None:
|
|
||||||
self.lcd.fill(self.lcd.black)
|
|
||||||
e = SCREENS[self.screen]["entity"]
|
|
||||||
if (e == None and d == None):
|
|
||||||
Font.cntr_st(self.lcd, self.lcd.width, "No media player selected", 110, 3, 255, 255, 255)
|
|
||||||
self.lcd.show()
|
|
||||||
return
|
|
||||||
if (d == None): d = self.api.getMediaPlayerData(e)
|
|
||||||
self.scr_vals[0] = d
|
|
||||||
self.lcd.fill(self.lcd.black)
|
|
||||||
Font.cntr_st(self.lcd, self.lcd.width, SCREENS[self.screen]["name"], 20, 2, 255, 255, 255)
|
|
||||||
self.__updateMediaPositionBar(d)
|
|
||||||
if (d["media_duration"] != None):
|
|
||||||
mins = d["media_duration"] // 60
|
|
||||||
secs = d["media_duration"] % 60
|
|
||||||
Font.rght_st(self.lcd, f"{mins}:{secs}", self.lcd.width, self.lcd.height - 16, 1, 180, 180, 180)
|
|
||||||
Font.cntr_st(self.lcd, self.lcd.width, d["media_title"], self.lcd.height - 72, 3, 255, 255, 255)
|
|
||||||
Font.cntr_st(self.lcd, self.lcd.width, d["media_artist"], self.lcd.height - 96, 2, 255, 255, 255)
|
|
||||||
self.lcd.show()
|
|
||||||
# TODO: display album art
|
|
||||||
# TODO: handle button presses for volume and changing tracks
|
|
||||||
# TODO: add icons next to buttons
|
|
||||||
|
|
||||||
def __displayUnknownScreen(self) -> None:
|
|
||||||
self.lcd.fill(self.lcd.black)
|
|
||||||
Font.cntr_st(self.lcd, self.lcd.width, "Invalid config", 110, 3, 255, 255, 255)
|
|
||||||
Font.cntr_st(self.lcd, self.lcd.width, "Screen " + str(self.screen), 130, 2, 255, 255, 255)
|
|
||||||
|
|
||||||
def __updateLightsScreen(self) -> None:
|
|
||||||
e = min(len(SCREENS[self.screen]["entities"]), 4)
|
|
||||||
# for each light to be displayed
|
|
||||||
for i in range(0, e):
|
|
||||||
# if its settings have changed, re-draw them without clearing the display
|
|
||||||
d = self.api.getLightData(SCREENS[self.screen]["entities"][i]["id"])
|
|
||||||
if (d != self.scr_vals[i]):
|
|
||||||
self.__displayLightEntity(i, self.lcd.width//2, self.lcd.height//2, self.lcd.width//2 * (i % 2), self.lcd.height//2 * (i//2), SCREENS[self.screen]["entities"][i]["name"], d)
|
|
||||||
# TODO: double button press to select a light, then up/down to change brightness? and single button click to turn on/off?
|
|
||||||
self.lcd.show()
|
|
||||||
|
|
||||||
def __handleLightsScreenButtons(self) -> bool:
|
|
||||||
# TODO: reset buttons used
|
|
||||||
b = [self.lcd.keyA["v"], self.lcd.keyB["v"], self.lcd.keyX["v"], self.lcd.keyY["v"]]
|
|
||||||
self.lcd.keyA["v"] = False
|
|
||||||
self.lcd.keyB["v"] = False
|
|
||||||
self.lcd.keyX["v"] = False
|
|
||||||
self.lcd.keyY["v"] = False
|
|
||||||
e = min(len(SCREENS[self.screen]["entities"]), 4)
|
|
||||||
a = False
|
|
||||||
for i in range(0, e):
|
|
||||||
# if button for light clicked, toggle the light
|
|
||||||
if (b[i]):
|
|
||||||
self.api.toggleLight(SCREENS[self.screen]["entities"][i]["id"])
|
|
||||||
a = True
|
|
||||||
return a
|
|
||||||
|
|
||||||
def __updateMediaScreen(self) -> None:
|
|
||||||
d = self.api.getMediaPlayerData(SCREENS[self.screen]["entity"])
|
|
||||||
# if same media is playing (same title and duration), just update the position bar
|
|
||||||
if (self.scr_vals[0]["media_title"] == d["media_title"] and self.scr_vals[0]["media_duration"] == d["media_duration"]):
|
|
||||||
self.__updateMediaPositionBar(d)
|
|
||||||
self.lcd.show()
|
|
||||||
# otherwise redraw the whole screen
|
|
||||||
else:
|
|
||||||
self.__displayMediaScreen(d)
|
|
||||||
|
|
||||||
def __handleMediaScreenButtons(self) -> bool:
|
|
||||||
up = self.lcd.up["v"]
|
up = self.lcd.up["v"]
|
||||||
down = self.lcd.down["v"]
|
down = self.lcd.down["v"]
|
||||||
|
left = self.lcd.left["v"]
|
||||||
|
right = self.lcd.right["v"]
|
||||||
|
ctrl = self.lcd.ctrl["v"]
|
||||||
keyA = self.lcd.keyA["v"]
|
keyA = self.lcd.keyA["v"]
|
||||||
|
keyB = self.lcd.keyB["v"]
|
||||||
keyX = self.lcd.keyX["v"]
|
keyX = self.lcd.keyX["v"]
|
||||||
keyY = self.lcd.keyY["v"]
|
keyY = self.lcd.keyY["v"]
|
||||||
self.lcd.up["v"] = False
|
self.__resetButtonStatuses()
|
||||||
self.lcd.down["v"] = False
|
if (ctrl):
|
||||||
self.lcd.keyA["v"] = False
|
reset()
|
||||||
self.lcd.keyX["v"] = False
|
return self.s.handleButtons(up, down, left, right, keyA, keyB, keyX, keyY, ctrl)
|
||||||
self.lcd.keyY["v"] = False
|
|
||||||
a = False
|
|
||||||
e = SCREENS[self.screen]["entity"]
|
|
||||||
if (up):
|
|
||||||
self.api.changeVolume(e)
|
|
||||||
a = True
|
|
||||||
elif (down):
|
|
||||||
self.api.changeVolume(e, False)
|
|
||||||
a = True
|
|
||||||
if (keyX):
|
|
||||||
self.api.nextTrack(e)
|
|
||||||
elif (keyY):
|
|
||||||
self.api.prevTrack(e)
|
|
||||||
if (keyA):
|
|
||||||
self.api.playPause(e)
|
|
||||||
return a
|
|
||||||
# if (a): self.__handleMediaScreenButtons()
|
|
||||||
|
|
||||||
def __manageScreen(self) -> None:
|
def __manageScreen(self) -> None:
|
||||||
started = False
|
started = False
|
||||||
while (True):
|
while (True):
|
||||||
gc.collect()
|
print("Mem free before and after collecting:")
|
||||||
if (time.localtime()[3] == 0): ntptime.settime()
|
print(mem_free())
|
||||||
|
collect()
|
||||||
|
print(mem_free())
|
||||||
|
if (localtime()[3] == 0):
|
||||||
|
try:
|
||||||
|
settime()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
changed = not started or self.__changeScreen()
|
changed = not started or self.__changeScreen()
|
||||||
if (not started): started = True
|
if (not started): started = True
|
||||||
# if the screen has changed, redraw the whole screen
|
# if the screen has changed, redraw the whole screen
|
||||||
if (changed):
|
if (changed):
|
||||||
if (SCREENS[self.screen]["type"] == 0):
|
|
||||||
self.__resetButtonStatuses()
|
self.__resetButtonStatuses()
|
||||||
self.__displayLightsScreen()
|
collect()
|
||||||
elif (SCREENS[self.screen]["type"] == 1):
|
if (SCREENS[self.scr_n]["type"] == 0):
|
||||||
self.__resetButtonStatuses()
|
self.s = LightsScreen(SCREENS[self.scr_n]["name"], SCREENS[self.scr_n]["entities"])
|
||||||
self.__displayMediaScreen()
|
elif (SCREENS[self.scr_n]["type"] == 1):
|
||||||
|
self.s = MediaScreen(SCREENS[self.scr_n]["name"], SCREENS[self.scr_n]["entity"])
|
||||||
else:
|
else:
|
||||||
self.__resetButtonStatuses()
|
self.s = UnknownScreen("Unknown")
|
||||||
self.__displayUnknownScreen()
|
self.s.display(self.lcd)
|
||||||
# otherwise minimise the number of pixels being changed
|
# otherwise minimise the number of pixels being changed
|
||||||
else:
|
else:
|
||||||
if (SCREENS[self.screen]["type"] == 0):
|
b = self.handleButtons()
|
||||||
if (self.__handleLightsScreenButtons()): continue
|
if (b): continue
|
||||||
self.__updateLightsScreen()
|
self.s.update(self.lcd)
|
||||||
elif (SCREENS[self.screen]["type"] == 1):
|
|
||||||
if (self.__handleMediaScreenButtons()): continue
|
|
||||||
self.__updateMediaScreen()
|
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
if (self.screen == None): return
|
if (self.scr_n == None): return
|
||||||
self.__boot()
|
self.__boot()
|
||||||
try:
|
try:
|
||||||
_thread.start_new_thread(self.__manageButtons, ())
|
start_new_thread(self.__manageButtons, ())
|
||||||
self.__manageScreen()
|
self.__manageScreen()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
machine.reset()
|
reset()
|
||||||
|
|||||||
23
font.py
23
font.py
@@ -1,4 +1,4 @@
|
|||||||
from utils import Utils
|
from utils import colour
|
||||||
|
|
||||||
# ===========Start of FONTS Section=========================
|
# ===========Start of FONTS Section=========================
|
||||||
# Standard ASCII 5x8 font
|
# Standard ASCII 5x8 font
|
||||||
@@ -264,10 +264,8 @@ FONT = bytes([
|
|||||||
0x00, 0x00, 0x00, 0x00, 0x00 # 255 also a <space>
|
0x00, 0x00, 0x00, 0x00, 0x00 # 255 also a <space>
|
||||||
])
|
])
|
||||||
|
|
||||||
class Font:
|
def character(lcd,asc,xt,yt,sz,r,g,b): # Single character sz is size: 1 or 2
|
||||||
@staticmethod
|
cc = colour(r,g,b)
|
||||||
def character(lcd,asc,xt,yt,sz,r,g,b): # Single character sz is size: 1 or 2
|
|
||||||
cc = Utils.colour(r,g,b)
|
|
||||||
code = asc * 5 # 5 bytes per character
|
code = asc * 5 # 5 bytes per character
|
||||||
for ii in range(5):
|
for ii in range(5):
|
||||||
line = FONT[code + ii]
|
line = FONT[code + ii]
|
||||||
@@ -285,30 +283,27 @@ class Font:
|
|||||||
lcd.pixel(ii*sz+xt+2,yy*sz+yt,cc)
|
lcd.pixel(ii*sz+xt+2,yy*sz+yt,cc)
|
||||||
lcd.pixel(ii*sz+xt+2,yy*sz+yt+1,cc)
|
lcd.pixel(ii*sz+xt+2,yy*sz+yt+1,cc)
|
||||||
|
|
||||||
@staticmethod
|
def prnt_st(lcd,asci,xx,yy,sz,r,g,b): # Text string
|
||||||
def prnt_st(lcd,asci,xx,yy,sz,r,g,b): # Text string
|
|
||||||
if sz == 1: move = 6
|
if sz == 1: move = 6
|
||||||
if sz == 2: move = 11
|
if sz == 2: move = 11
|
||||||
if sz == 3: move = 17
|
if sz == 3: move = 17
|
||||||
for letter in(asci):
|
for letter in(asci):
|
||||||
asci = ord(letter)
|
asci = ord(letter)
|
||||||
Font.character(lcd,asci,xx,yy,sz,r,g,b)
|
character(lcd,asci,xx,yy,sz,r,g,b)
|
||||||
xx = xx + move
|
xx = xx + move
|
||||||
|
|
||||||
@staticmethod
|
def cntr_st(lcd,width,txt,y,size,r,g,b,o=0): # Centres text on line y, skipping first o pixels
|
||||||
def cntr_st(lcd,width,txt,y,size,r,g,b,o=0): # Centres text on line y, skipping first o pixels
|
|
||||||
if size == 1: w = 6
|
if size == 1: w = 6
|
||||||
if size == 2: w = 11
|
if size == 2: w = 11
|
||||||
if size == 3: w = 17
|
if size == 3: w = 17
|
||||||
gap = (width - len(txt) * w)//2 + o
|
gap = (width - len(txt) * w)//2 + o
|
||||||
Font.prnt_st(lcd,txt,gap,y,size,r,g,b)
|
prnt_st(lcd,txt,gap,y,size,r,g,b)
|
||||||
|
|
||||||
@staticmethod
|
def rght_st(lcd,asci,xx,yy,sz,r,g,b):
|
||||||
def rght_st(lcd,asci,xx,yy,sz,r,g,b):
|
|
||||||
if sz == 1: w = 6
|
if sz == 1: w = 6
|
||||||
if sz == 2: w = 11
|
if sz == 2: w = 11
|
||||||
if sz == 3: w = 17
|
if sz == 3: w = 17
|
||||||
xo = xx - len(asci) * w
|
xo = xx - len(asci) * w
|
||||||
Font.prnt_st(lcd,asci,xo,yy,sz,r,g,b)
|
prnt_st(lcd,asci,xo,yy,sz,r,g,b)
|
||||||
|
|
||||||
# =========== End of font support routines ===========
|
# =========== End of font support routines ===========
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from machine import Pin,SPI,PWM
|
from machine import Pin,SPI,PWM
|
||||||
from font import Font
|
from framebuf import FrameBuffer, RGB565
|
||||||
from utils import Utils
|
|
||||||
import framebuf
|
|
||||||
|
|
||||||
BL = 13
|
BL = 13
|
||||||
DC = 8
|
DC = 8
|
||||||
@@ -11,7 +9,7 @@ SCK = 10
|
|||||||
CS = 9
|
CS = 9
|
||||||
|
|
||||||
# LCD driver
|
# LCD driver
|
||||||
class LCD(framebuf.FrameBuffer):
|
class LCD(FrameBuffer):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pwm = PWM(Pin(BL))
|
pwm = PWM(Pin(BL))
|
||||||
pwm.freq(1000)
|
pwm.freq(1000)
|
||||||
@@ -30,15 +28,9 @@ class LCD(framebuf.FrameBuffer):
|
|||||||
self.dc = Pin(DC,Pin.OUT)
|
self.dc = Pin(DC,Pin.OUT)
|
||||||
self.dc(1)
|
self.dc(1)
|
||||||
self.buffer = bytearray(self.height * self.width * 2)
|
self.buffer = bytearray(self.height * self.width * 2)
|
||||||
super().__init__(self.buffer, self.width, self.height, framebuf.RGB565)
|
super().__init__(self.buffer, self.width, self.height, RGB565)
|
||||||
self.init_display()
|
self.init_display()
|
||||||
|
|
||||||
self.red = 0x07E0
|
|
||||||
self.green = 0x001f
|
|
||||||
self.blue = 0xf800
|
|
||||||
self.white = 0xffff
|
|
||||||
self.black = 0x0000
|
|
||||||
|
|
||||||
self.keyA = {
|
self.keyA = {
|
||||||
"v": False, # value
|
"v": False, # value
|
||||||
"p": Pin(15,Pin.IN,Pin.PULL_UP) # pin - normally 1 but 0 if pressed
|
"p": Pin(15,Pin.IN,Pin.PULL_UP) # pin - normally 1 but 0 if pressed
|
||||||
@@ -194,6 +186,3 @@ class LCD(framebuf.FrameBuffer):
|
|||||||
self.spi.write(self.buffer)
|
self.spi.write(self.buffer)
|
||||||
self.cs(1)
|
self.cs(1)
|
||||||
# END OF DRIVER
|
# END OF DRIVER
|
||||||
|
|
||||||
def clear(self, c):
|
|
||||||
self.fill(c)
|
|
||||||
212
screens.py
Normal file
212
screens.py
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
from font import cntr_st, rght_st
|
||||||
|
from utils import colour
|
||||||
|
from bmp_file_reader import BMPFileReader
|
||||||
|
from api import getMediaPlayerData, getLightData, playPause, nextTrack, prevTrack, changeVolume, setVolume, toggleLight, setBrightness
|
||||||
|
|
||||||
|
class Screen():
|
||||||
|
def __init__(self, n: str) -> None:
|
||||||
|
self.name = n
|
||||||
|
self.d = {}
|
||||||
|
self.prev = {}
|
||||||
|
|
||||||
|
def display(self, lcd) -> None:
|
||||||
|
lcd.fill(0x0000)
|
||||||
|
cntr_st(lcd, lcd.width, self.name, 20, 2, 255, 255, 255)
|
||||||
|
|
||||||
|
def update(self, lcd) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handleButtons(self, up: bool, down: bool, left: bool, right: bool, keyA: bool, keyB: bool, keyX: bool, keyY: bool, ctrl: bool) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _updateData(self) -> dict:
|
||||||
|
self.prev = self.d.copy()
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _invalidConfig(self, lcd) -> None:
|
||||||
|
cntr_st(lcd, lcd.width, "Invalid config", lcd.height//2, 2, 255, 255, 255)
|
||||||
|
lcd.show()
|
||||||
|
|
||||||
|
class MediaScreen(Screen):
|
||||||
|
def __init__(self, n: str, e: str) -> None:
|
||||||
|
super().__init__(n)
|
||||||
|
self.e = e
|
||||||
|
self.valid = e != None and e != ""
|
||||||
|
|
||||||
|
def display(self, lcd) -> None:
|
||||||
|
super().display(lcd)
|
||||||
|
if (not self.valid):
|
||||||
|
self._invalidConfig(lcd)
|
||||||
|
return
|
||||||
|
if (self.d == {}):
|
||||||
|
self.d = self._updateData()
|
||||||
|
self.__updateMediaPositionBar(lcd, self.d["media_position"], self.d["media_duration"])
|
||||||
|
if (self.d["media_duration"] != None):
|
||||||
|
mins = self.d["media_duration"] // 60
|
||||||
|
secs = self.d["media_duration"] % 60
|
||||||
|
rght_st(lcd, f"{mins}:{secs}", lcd.width, lcd.height - 16, 1, 180, 180, 180)
|
||||||
|
cntr_st(lcd, lcd.width, self.d["media_title"], lcd.height - 72, 3, 255, 255, 255)
|
||||||
|
cntr_st(lcd, lcd.width, self.d["media_artist"], lcd.height - 96, 2, 255, 255, 255)
|
||||||
|
lcd.show()
|
||||||
|
|
||||||
|
def __updateMediaPositionBar(self, lcd, p: int, d: int):
|
||||||
|
if (d > 0):
|
||||||
|
for x in range (0, (lcd.width * p)//d):
|
||||||
|
lcd.pixel(x, lcd.height - 5, 0xffff)
|
||||||
|
lcd.pixel(x, lcd.height - 4, 0xffff)
|
||||||
|
lcd.pixel(x, lcd.height - 3, 0xffff)
|
||||||
|
lcd.pixel(x, lcd.height - 2, 0xffff)
|
||||||
|
lcd.pixel(x, lcd.height - 1, 0xffff)
|
||||||
|
lcd.pixel(x, lcd.height, 0xffff)
|
||||||
|
|
||||||
|
def update(self, lcd):
|
||||||
|
if (not self.valid):
|
||||||
|
super().display(lcd)
|
||||||
|
self._invalidConfig(lcd)
|
||||||
|
return
|
||||||
|
self._updateData()
|
||||||
|
# if same media is playing (same title and duration), just update the position bar
|
||||||
|
if (self.d["media_title"] == self.prev["media_title"] and self.d["media_duration"] == self.prev["media_duration"]):
|
||||||
|
self.__updateMediaPositionBar(lcd, self.d["media_position"], self.d["media_duration"])
|
||||||
|
lcd.show()
|
||||||
|
# otherwise redraw the whole screen
|
||||||
|
else:
|
||||||
|
self.display(lcd)
|
||||||
|
|
||||||
|
def _updateData(self) -> dict:
|
||||||
|
super()._updateData()
|
||||||
|
self.d = getMediaPlayerData(self.e)
|
||||||
|
return self.d
|
||||||
|
|
||||||
|
def handleButtons(self, up: bool, down: bool, left: bool, right: bool, keyA: bool, keyB: bool, keyX: bool, keyY: bool, ctrl: bool) -> bool:
|
||||||
|
a = False
|
||||||
|
v = self.d["volume_level"] if "volume_level" in self.d else None
|
||||||
|
if (v != None and up and v < 1):
|
||||||
|
vn = min(1.0, v + 0.08)
|
||||||
|
setVolume(self.e, vn)
|
||||||
|
self.d["volume_level"] = vn
|
||||||
|
a = True
|
||||||
|
elif (v != None and down and v > 0.01):
|
||||||
|
vn = max(0.01, v - 0.08)
|
||||||
|
setVolume(self.e, vn)
|
||||||
|
self.d["volume_level"] = vn
|
||||||
|
a = True
|
||||||
|
if (keyX):
|
||||||
|
nextTrack(self.e)
|
||||||
|
elif (keyY):
|
||||||
|
prevTrack(self.e)
|
||||||
|
if (keyA):
|
||||||
|
playPause(self.e)
|
||||||
|
return a
|
||||||
|
|
||||||
|
class LightsScreen(Screen):
|
||||||
|
def __init__(self, n: str, es: list) -> None:
|
||||||
|
super().__init__(n)
|
||||||
|
self.es = es
|
||||||
|
self.valid = es != None and len(es) != 0
|
||||||
|
|
||||||
|
def display(self, lcd) -> None:
|
||||||
|
super().display(lcd)
|
||||||
|
if (not self.valid):
|
||||||
|
self._invalidConfig(lcd)
|
||||||
|
return
|
||||||
|
if (self.d == {}):
|
||||||
|
self._updateData()
|
||||||
|
# display up to four lights as defined in env.py
|
||||||
|
# for each defined entity (up to a max total of 4), display its data in a 2x2 grid
|
||||||
|
for i in range(0, len(self.d)):
|
||||||
|
self.__displayLightEntity(lcd, i, lcd.width//2, lcd.height//2, lcd.width//2 * (i % 2), lcd.height//2 * (i//2), self.es[i]["name"], self.d[i])
|
||||||
|
lcd.show()
|
||||||
|
|
||||||
|
def __displayLightEntity(self, lcd, i: int, w: int, h: int, xo: int, yo: int, n: str, d) -> None:
|
||||||
|
# if the light is turned on, display the filled-in lightbulb icon in the colour of the light, centrally in the light's grid square
|
||||||
|
if (d["on"]):
|
||||||
|
color = colour(d["rgb_color"][0], d["rgb_color"][1], d["rgb_color"][2])
|
||||||
|
with open("images/lightbulb-on.bmp", "rb") as file_handle:
|
||||||
|
reader = BMPFileReader(file_handle)
|
||||||
|
img_height = reader.get_height()
|
||||||
|
x_offset = w//2 + xo - reader.get_width()//2
|
||||||
|
y_offset = h//2 + yo - reader.get_height()//2 - 4
|
||||||
|
for row_i in range(0, reader.get_height()):
|
||||||
|
row = reader.get_row(row_i)
|
||||||
|
for col_i, color in enumerate(row):
|
||||||
|
r = d["rgb_color"][0] if (color.red) != 0 else 0
|
||||||
|
g = d["rgb_color"][1] if (color.green) != 0 else 0
|
||||||
|
b = d["rgb_color"][2] if (color.blue) != 0 else 0
|
||||||
|
if (color.red != 0 or color.green != 0 or color.blue != 0):
|
||||||
|
lcd.pixel(col_i + x_offset, row_i + y_offset, colour(r,g,b))
|
||||||
|
# otherwise display the outline lightbulb icon in grey, centrally in the light's grid square
|
||||||
|
else:
|
||||||
|
color = colour(80, 80, 80)
|
||||||
|
print("Drawing light off")
|
||||||
|
with open("images/lightbulb-off.bmp", "rb") as file_handle:
|
||||||
|
reader = BMPFileReader(file_handle)
|
||||||
|
print("Drawing light off - loaded file")
|
||||||
|
img_height = reader.get_height()
|
||||||
|
x_offset = w//2 + xo - reader.get_width()//2
|
||||||
|
y_offset = h//2 + yo - reader.get_height()//2 - 4
|
||||||
|
for row_i in range(0, reader.get_height()):
|
||||||
|
row = reader.get_row(row_i)
|
||||||
|
for col_i, color in enumerate(row):
|
||||||
|
if (color.red != 0 or color.green != 0 or color.blue != 0):
|
||||||
|
lcd.pixel(col_i + x_offset, row_i + y_offset, colour(color.red, color.green, color.blue))
|
||||||
|
else:
|
||||||
|
lcd.pixel(col_i + x_offset, row_i + y_offset, colour(0,0,0))
|
||||||
|
# display the name of the light 8px below the lightbulb icon
|
||||||
|
cntr_st(lcd, w, n, y_offset + img_height + 8, 2, 220, 220, 220, xo)
|
||||||
|
|
||||||
|
def update(self, lcd):
|
||||||
|
if (not self.valid):
|
||||||
|
super().display(lcd)
|
||||||
|
self._invalidConfig(lcd)
|
||||||
|
return
|
||||||
|
self._updateData()
|
||||||
|
print("Updating lights")
|
||||||
|
# for each light to be displayed
|
||||||
|
for i in range(0, len(self.d)):
|
||||||
|
# if its settings have changed, re-draw them without clearing the display
|
||||||
|
if (self.d[i] != self.prev[i]):
|
||||||
|
self.__displayLightEntity(lcd, i, lcd.width//2, lcd.height//2, lcd.width//2 * (i % 2), lcd.height//2 * (i//2), self.es[i]["name"], self.d[i])
|
||||||
|
lcd.show()
|
||||||
|
|
||||||
|
def _updateData(self) -> dict:
|
||||||
|
super()._updateData()
|
||||||
|
for i in range(0, min(len(self.es), 4)):
|
||||||
|
self.d[i] = getLightData(self.es[i]["id"])
|
||||||
|
return self.d
|
||||||
|
|
||||||
|
def handleButtons(self, up: bool, down: bool, left: bool, right: bool, keyA: bool, keyB: bool, keyX: bool, keyY: bool, ctrl: bool) -> bool:
|
||||||
|
e = min(len(self.es), 4)
|
||||||
|
b = [keyA, keyB, keyX, keyY]
|
||||||
|
a = False
|
||||||
|
for i in range(0, e):
|
||||||
|
# if button for light clicked, toggle the light
|
||||||
|
if (b[i]):
|
||||||
|
toggleLight(self.es[i]["id"])
|
||||||
|
a = True
|
||||||
|
# if up/down clicked, adjust brightness for all lights that are turned on
|
||||||
|
pcfg = i in self.prev and "on" in self.prev[i] and self.prev[i]["on"] and "brightness" in self.prev[i]
|
||||||
|
if (up and pcfg):
|
||||||
|
setBrightness(self.es[i]["id"], min(255, self.prev[i]["brightness"] + 35))
|
||||||
|
a = True
|
||||||
|
elif (down and pcfg):
|
||||||
|
setBrightness(self.es[i]["id"], max(1, self.prev[i]["brightness"] - 35))
|
||||||
|
a = True
|
||||||
|
return a
|
||||||
|
|
||||||
|
class UnknownScreen(Screen):
|
||||||
|
def __init__(self, n: str) -> None:
|
||||||
|
super().__init__(n)
|
||||||
|
|
||||||
|
def display(self, lcd) -> None:
|
||||||
|
super().display(lcd)
|
||||||
|
self._invalidConfig(lcd)
|
||||||
|
|
||||||
|
def update(self, lcd) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _updateData(self) -> dict:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def handleButtons(self, up: bool, down: bool, left: bool, right: bool, keyA: bool, keyB: bool, keyX: bool, keyY: bool, ctrl: bool) -> bool:
|
||||||
|
return False
|
||||||
6
utils.py
6
utils.py
@@ -1,5 +1,3 @@
|
|||||||
class Utils:
|
# method from Tony Goodhew 21st April 2022, for thepihut.com
|
||||||
# method from Tony Goodhew 21st April 2022, for thepihut.com
|
def colour(R,G,B): # Convert RGB888 to RGB565
|
||||||
@staticmethod
|
|
||||||
def colour(R,G,B): # Convert RGB888 to RGB565
|
|
||||||
return (((G&0b00011100)<<3) +((B&0b11111000)>>3)<<8) + (R&0b11111000)+((G&0b11100000)>>5)
|
return (((G&0b00011100)<<3) +((B&0b11111000)>>3)<<8) + (R&0b11111000)+((G&0b11100000)>>5)
|
||||||
Reference in New Issue
Block a user