diff --git a/api.py b/api.py index 5b32bc9..ef6f3a8 100644 --- a/api.py +++ b/api.py @@ -1,84 +1,94 @@ from requests import get, post -import gc -import time +from gc import collect +from time import mktime, localtime +from env import HASS_URL, TOKEN -class Api: - def __init__(self, base_url, access_token) -> None: - self.base_url = base_url - self.access_token = access_token - - def __request(self, endpoint) -> dict: - gc.collect() - url = self.base_url + endpoint - headers = { - "Authorization": "Bearer " + self.access_token, - "content-type": "application/json" - } - print("Starting request for " + endpoint) - response = get(url, headers=headers) +def getReq(endpoint) -> dict: + collect() + headers = { + "Authorization": "Bearer " + TOKEN, + "content-type": "application/json" + } + print("Starting request for " + endpoint) + try: + response = get(HASS_URL + endpoint, headers=headers) return response.json() + except: + return {} - def __post(self, endpoint, d) -> bool: - gc.collect() - url = self.base_url + endpoint - headers = { - "Authorization": "Bearer " + self.access_token, - "content-type": "application/json" - } - print("Starting post request to " + endpoint) - response = post(url, headers=headers, json=d) +def postReq(endpoint, d) -> bool: + collect() + headers = { + "Authorization": "Bearer " + TOKEN, + "content-type": "application/json" + } + print("Starting post request to " + endpoint) + try: + response = post(HASS_URL + endpoint, headers=headers, json=d) return response.status_code == 200 - - def getLightData(self, entity_id: str) -> dict: - # TODO: error handling if can't access hass (e.g. no connection, invalid token) - response = self.__request("/api/states/" + entity_id) - on = response["state"] == "on" - return { - "on": on, - "rgb_color": response["attributes"]["rgb_color"] if on else None, - "rgbw_color": response["attributes"]["rgbw_color"] if on else None, - "brightness": response["attributes"]["brightness"] if on else 0, - "friendly_name": response["attributes"]["friendly_name"] - } - - def getMediaPlayerData(self, entity_id: str) -> dict: - response = self.__request("/api/states/" + entity_id) - if ("media_position" in (dict)(response["attributes"])): - p = response["attributes"]["media_position"] - else: - p = 0 - if ("media_position_updated_at" in (dict)(response["attributes"])): - 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)) - p += time.mktime(time.localtime()) - t - return { - "playing": response["state"] in ["on", "playing", "buffering"], - "shuffle": response["attributes"]["shuffle"], - "repeat": response["attributes"]["repeat"] == "on", - "volume_level": response["attributes"]["volume_level"], - "friendly_name": response["attributes"]["friendly_name"], - "entity_picture": response["attributes"]["entity_picture"] if "entity_picture" in (dict)(response["attributes"]) else None, - "media_duration": response["attributes"]["media_duration"] if "media_duration" in (dict)(response["attributes"]) else 0, - "media_position": p, - "media_title": response["attributes"]["media_title"] if "media_title" in (dict)(response["attributes"]) else "Nothing playing", - "media_artist": response["attributes"]["media_artist"] if "media_artist" in (dict)(response["attributes"]) else "", - "media_album_name": response["attributes"]["media_album_name"] if "media_album_name" in (dict)(response["attributes"]) else "" - } - - def changeVolume(self, entity_id: str, up: bool = True) -> None: - if (up): dir = "up" - else: dir = "down" - self.__post(f"/api/services/media_player/volume_{dir}", {"entity_id": entity_id}) - self.__post(f"/api/services/media_player/volume_{dir}", {"entity_id": entity_id}) - - def nextTrack(self, entity_id: str) -> None: - self.__post("/api/services/media_player/media_next_track", {"entity_id": entity_id}) - - def prevTrack(self, entity_id: str) -> None: - self.__post("/api/services/media_player/media_previous_track", {"entity_id": entity_id}) + except: + return False - def playPause(self, entity_id: str) -> None: - self.__post("/api/services/media_player/media_play_pause", {"entity_id": entity_id}) +def getLightData(entity_id: str) -> dict: + # TODO: error handling if can't access hass (e.g. no connection, invalid token) + response = getReq("/api/states/" + entity_id) + on = "state" in response and response["state"] == "on" + return { + "on": on, + "rgb_color": response["attributes"]["rgb_color"] if on else None, + "rgbw_color": response["attributes"]["rgbw_color"] if on else None, + "brightness": response["attributes"]["brightness"] if on else 0.0 + } - def toggleLight(self, entity_id: str) -> None: - self.__post("/api/services/light/toggle", {"entity_id": entity_id}) +def getMediaPlayerData(entity_id: str) -> dict: + 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"])): + p = response["attributes"]["media_position"] + else: + p = 0 + if ("media_position_updated_at" in (dict)(response["attributes"])): + dts = response["attributes"]["media_position_updated_at"] + 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 += mktime(localtime()) - t + return { + "playing": response["state"] in ["on", "playing", "buffering"], + "shuffle": response["attributes"]["shuffle"] if e and "shuffle" in response["attributes"] else False, + "repeat": response["attributes"]["repeat"] == "on" if e and "repeat" in response["attributes"] else False, + "volume_level": response["attributes"]["volume_level"] if e and "volume_level" in response["attributes"] else 0, + "entity_picture": response["attributes"]["entity_picture"] if e and "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_position": p, + "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 e and "media_artist" 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(entity_id: str, up: bool = True) -> None: + if (up): dir = "up" + else: dir = "down" + postReq(f"/api/services/media_player/volume_{dir}", {"entity_id": entity_id}) + postReq(f"/api/services/media_player/volume_{dir}", {"entity_id": entity_id}) + +def setVolume(entity_id: str, v: float) -> None: + postReq(f"/api/services/media_player/volume_set", {"entity_id": entity_id, "volume_level": v}) + +def nextTrack(entity_id: str) -> None: + postReq("/api/services/media_player/media_next_track", {"entity_id": entity_id}) + +def prevTrack(entity_id: str) -> None: + postReq("/api/services/media_player/media_previous_track", {"entity_id": entity_id}) + +def playPause(entity_id: str) -> None: + 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}) diff --git a/app.py b/app.py index b2bab3b..67c299d 100644 --- a/app.py +++ b/app.py @@ -1,29 +1,24 @@ -from api import Api -import machine -import network -from time import sleep -from screen import LCD -from font import Font -import gc -from env import * -from utils import Utils -import _thread -import bmp_file_reader as bmpr -import ntptime -import time +from machine import reset +from network import WLAN, STA_IF +from time import sleep, localtime +from lcd import LCD +from font import cntr_st +from gc import collect, mem_free +from env import HOSTNAME, SSID, WIFI_PASSWORD, SCREENS +from _thread import start_new_thread +from ntptime import settime +from screens import * class App: def __init__(self) -> None: if (len(SCREENS) == 0): print("No screens configured. Check env.py.") - self.screen = -1 - else: self.screen = 0 + self.scr_n = -1 + else: self.scr_n = 0 self.lcd = LCD() - self.api = Api(HASS_URL, TOKEN) - self.scr_vals = {} # stored values from the current screen def __connect(self) -> int: - wlan = network.WLAN(network.STA_IF) + wlan = WLAN(STA_IF) wlan.active(True) wlan.config(hostname=HOSTNAME) wlan.connect(SSID, WIFI_PASSWORD) @@ -35,14 +30,17 @@ class App: return self.ip def __boot(self) -> None: - self.lcd.fill(self.lcd.black) - Font.cntr_st(self.lcd, self.lcd.width, "Booting...", 120, 2, 150, 150, 150) + collect() + print("Booting") + self.lcd.fill(0x0000) + cntr_st(self.lcd, self.lcd.width, "Booting...", 120, 2, 150, 150, 150) self.lcd.show() self.__connect() - print("Local time before synchronization:%s" %str(time.localtime())) - ntptime.host = "1.europe.pool.ntp.org" - ntptime.settime() - print("Local time after synchronization:%s" %str(time.localtime())) + try: + settime() + print("Local time after synchronization: %s" %str(localtime())) + except: + pass def __resetButtonStatuses(self) -> None: self.lcd.keyA["v"] = False @@ -89,209 +87,75 @@ class App: sleep(0.2) def __changeScreen(self) -> bool: - orig = self.screen + c = False if (self.lcd.right["v"]): - if (self.screen == len(SCREENS) - 1): - self.screen = 0 + c = True + if (self.scr_n == len(SCREENS) - 1): + self.scr_n = 0 else: - self.screen += 1 + self.scr_n += 1 elif (self.lcd.left["v"]): - if (self.screen == 0): - self.screen = len(SCREENS) - 1 + c = True + if (self.scr_n == 0): + self.scr_n = len(SCREENS) - 1 else: - self.screen -= 1 - change = self.screen != orig - if (change): - self.scr_vals = {} + self.scr_n -= 1 self.lcd.left["v"] = False self.lcd.right["v"] = False - print(f"Screen change: {change}") - return change - - def __displayLightEntity(self, i: int, w: int, h: int, xo: int, yo: int, n: str, d) -> None: - 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 + print(f"Screen change: {c}") + return c - 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: + def handleButtons(self) -> bool: up = self.lcd.up["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"] + keyB = self.lcd.keyB["v"] keyX = self.lcd.keyX["v"] keyY = self.lcd.keyY["v"] - self.lcd.up["v"] = False - self.lcd.down["v"] = False - self.lcd.keyA["v"] = False - self.lcd.keyX["v"] = False - 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() + self.__resetButtonStatuses() + if (ctrl): + reset() + return self.s.handleButtons(up, down, left, right, keyA, keyB, keyX, keyY, ctrl) def __manageScreen(self) -> None: started = False while (True): - gc.collect() - if (time.localtime()[3] == 0): ntptime.settime() + print("Mem free before and after collecting:") + print(mem_free()) + collect() + print(mem_free()) + if (localtime()[3] == 0): + try: + settime() + except: + pass changed = not started or self.__changeScreen() if (not started): started = True # if the screen has changed, redraw the whole screen if (changed): - if (SCREENS[self.screen]["type"] == 0): - self.__resetButtonStatuses() - self.__displayLightsScreen() - elif (SCREENS[self.screen]["type"] == 1): - self.__resetButtonStatuses() - self.__displayMediaScreen() + self.__resetButtonStatuses() + collect() + if (SCREENS[self.scr_n]["type"] == 0): + self.s = LightsScreen(SCREENS[self.scr_n]["name"], SCREENS[self.scr_n]["entities"]) + elif (SCREENS[self.scr_n]["type"] == 1): + self.s = MediaScreen(SCREENS[self.scr_n]["name"], SCREENS[self.scr_n]["entity"]) else: - self.__resetButtonStatuses() - self.__displayUnknownScreen() + self.s = UnknownScreen("Unknown") + self.s.display(self.lcd) # otherwise minimise the number of pixels being changed else: - if (SCREENS[self.screen]["type"] == 0): - if (self.__handleLightsScreenButtons()): continue - self.__updateLightsScreen() - elif (SCREENS[self.screen]["type"] == 1): - if (self.__handleMediaScreenButtons()): continue - self.__updateMediaScreen() + b = self.handleButtons() + if (b): continue + self.s.update(self.lcd) def run(self) -> None: - if (self.screen == None): return + if (self.scr_n == None): return self.__boot() try: - _thread.start_new_thread(self.__manageButtons, ()) + start_new_thread(self.__manageButtons, ()) self.__manageScreen() except KeyboardInterrupt: - machine.reset() + reset() diff --git a/font.py b/font.py index eb6159f..871a486 100644 --- a/font.py +++ b/font.py @@ -1,4 +1,4 @@ -from utils import Utils +from utils import colour # ===========Start of FONTS Section========================= # Standard ASCII 5x8 font @@ -264,51 +264,46 @@ FONT = bytes([ 0x00, 0x00, 0x00, 0x00, 0x00 # 255 also a ]) -class Font: - @staticmethod - 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 - for ii in range(5): - line = FONT[code + ii] - for yy in range(8): - if (line >> yy) & 0x1: - lcd.pixel(ii*sz+xt,yy*sz+yt,cc) - if sz > 1: - lcd.pixel(ii*sz+xt+1,yy*sz+yt,cc) - lcd.pixel(ii*sz+xt,yy*sz+yt+1,cc) - lcd.pixel(ii*sz+xt+1,yy*sz+yt+1,cc) - if sz == 3: - lcd.pixel(ii*sz+xt, yy*sz+yt+2,cc) - lcd.pixel(ii*sz+xt+1,yy*sz+yt+2,cc) - lcd.pixel(ii*sz+xt+2,yy*sz+yt+2,cc) - lcd.pixel(ii*sz+xt+2,yy*sz+yt,cc) - lcd.pixel(ii*sz+xt+2,yy*sz+yt+1,cc) +def character(lcd,asc,xt,yt,sz,r,g,b): # Single character sz is size: 1 or 2 + cc = colour(r,g,b) + code = asc * 5 # 5 bytes per character + for ii in range(5): + line = FONT[code + ii] + for yy in range(8): + if (line >> yy) & 0x1: + lcd.pixel(ii*sz+xt,yy*sz+yt,cc) + if sz > 1: + lcd.pixel(ii*sz+xt+1,yy*sz+yt,cc) + lcd.pixel(ii*sz+xt,yy*sz+yt+1,cc) + lcd.pixel(ii*sz+xt+1,yy*sz+yt+1,cc) + if sz == 3: + lcd.pixel(ii*sz+xt, yy*sz+yt+2,cc) + lcd.pixel(ii*sz+xt+1,yy*sz+yt+2,cc) + lcd.pixel(ii*sz+xt+2,yy*sz+yt+2,cc) + lcd.pixel(ii*sz+xt+2,yy*sz+yt,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 - if sz == 1: move = 6 - if sz == 2: move = 11 - if sz == 3: move = 17 - for letter in(asci): - asci = ord(letter) - Font.character(lcd,asci,xx,yy,sz,r,g,b) - xx = xx + move +def prnt_st(lcd,asci,xx,yy,sz,r,g,b): # Text string + if sz == 1: move = 6 + if sz == 2: move = 11 + if sz == 3: move = 17 + for letter in(asci): + asci = ord(letter) + character(lcd,asci,xx,yy,sz,r,g,b) + 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 - if size == 1: w = 6 - if size == 2: w = 11 - if size == 3: w = 17 - gap = (width - len(txt) * w)//2 + o - Font.prnt_st(lcd,txt,gap,y,size,r,g,b) +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 == 2: w = 11 + if size == 3: w = 17 + gap = (width - len(txt) * w)//2 + o + prnt_st(lcd,txt,gap,y,size,r,g,b) - @staticmethod - def rght_st(lcd,asci,xx,yy,sz,r,g,b): - if sz == 1: w = 6 - if sz == 2: w = 11 - if sz == 3: w = 17 - xo = xx - len(asci) * w - Font.prnt_st(lcd,asci,xo,yy,sz,r,g,b) +def rght_st(lcd,asci,xx,yy,sz,r,g,b): + if sz == 1: w = 6 + if sz == 2: w = 11 + if sz == 3: w = 17 + xo = xx - len(asci) * w + prnt_st(lcd,asci,xo,yy,sz,r,g,b) # =========== End of font support routines =========== diff --git a/screen.py b/lcd.py similarity index 92% rename from screen.py rename to lcd.py index 8c69798..86e4791 100644 --- a/screen.py +++ b/lcd.py @@ -1,7 +1,5 @@ from machine import Pin,SPI,PWM -from font import Font -from utils import Utils -import framebuf +from framebuf import FrameBuffer, RGB565 BL = 13 DC = 8 @@ -11,7 +9,7 @@ SCK = 10 CS = 9 # LCD driver -class LCD(framebuf.FrameBuffer): +class LCD(FrameBuffer): def __init__(self): pwm = PWM(Pin(BL)) pwm.freq(1000) @@ -30,14 +28,8 @@ class LCD(framebuf.FrameBuffer): self.dc = Pin(DC,Pin.OUT) self.dc(1) 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.red = 0x07E0 - self.green = 0x001f - self.blue = 0xf800 - self.white = 0xffff - self.black = 0x0000 self.keyA = { "v": False, # value @@ -194,6 +186,3 @@ class LCD(framebuf.FrameBuffer): self.spi.write(self.buffer) self.cs(1) # END OF DRIVER - - def clear(self, c): - self.fill(c) diff --git a/screens.py b/screens.py new file mode 100644 index 0000000..8308cd5 --- /dev/null +++ b/screens.py @@ -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 diff --git a/utils.py b/utils.py index b701052..10eb41a 100644 --- a/utils.py +++ b/utils.py @@ -1,5 +1,3 @@ -class Utils: - # method from Tony Goodhew 21st April 2022, for thepihut.com - @staticmethod - def colour(R,G,B): # Convert RGB888 to RGB565 - return (((G&0b00011100)<<3) +((B&0b11111000)>>3)<<8) + (R&0b11111000)+((G&0b11100000)>>5) \ No newline at end of file +# method from Tony Goodhew 21st April 2022, for thepihut.com +def colour(R,G,B): # Convert RGB888 to RGB565 + return (((G&0b00011100)<<3) +((B&0b11111000)>>3)<<8) + (R&0b11111000)+((G&0b11100000)>>5) \ No newline at end of file