From 0b2c69239216780bea976581a703c8bb47d600c5 Mon Sep 17 00:00:00 2001 From: Ronald Date: Sun, 17 Nov 2024 22:05:30 +0000 Subject: [PATCH] Major rewrite and refactor under way Nearly there, needs some more testing around the WaveShare and Terminal screen classes. --- .gitignore | 3 + 04B_08__.TTF | Bin 19020 -> 0 bytes libreview/__init__.py | 2 + libreview/libreview.py | 114 ++++++++++ main.py | 340 ++++++++++------------------ screens/__init__.py | 3 + screens/debug_screens.py | 65 ++++++ {drive => screens/drive}/SSD1305.py | 0 {drive => screens/drive}/config.py | 0 screens/screen.py | 14 ++ screens/terminal_screen.py | 28 +++ screens/waveshare_233_ssd1305.py | 56 +++++ 12 files changed, 409 insertions(+), 216 deletions(-) create mode 100644 .gitignore delete mode 100644 04B_08__.TTF create mode 100644 libreview/__init__.py create mode 100644 libreview/libreview.py create mode 100644 screens/__init__.py create mode 100644 screens/debug_screens.py rename {drive => screens/drive}/SSD1305.py (100%) rename {drive => screens/drive}/config.py (100%) create mode 100644 screens/screen.py create mode 100644 screens/terminal_screen.py create mode 100644 screens/waveshare_233_ssd1305.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ebd098 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +screens/__pycache__ +libreview/__pycache__ +screens/04B_08__.TTF diff --git a/04B_08__.TTF b/04B_08__.TTF deleted file mode 100644 index aadf20e3b988f0b78db5a4a40c78f8ce504fb58e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19020 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 1: logging.fatal("currently glucose-monitor only supports accounts with one connection") + sys.exit(1) if len(libreview_session.connections) < 1: - logging.fatal("to use glucose-monitor your libreview account must have one connection") + logging.fatal("to use glucose-monitor your libreview account must have at least one connection") + sys.exit(1) patient_id = libreview_session.connections[0]['patientId'] - first_run = True - while not SHOULD_EXIT: - if not SHOULD_EXIT and not first_run: - time.sleep(delay) - elif first_run: - if show_in_terminal: - clear_terminal() - - if show_on_screen: - clear_display() + if display.supports_custom_fonts: + if font != "DEFAULT": + display.setFont(font, int(font_size)) + else: + display.setDefaultFont() - first_run = False + while not SHOULD_EXIT: + display.clearScreen() try: graph = libreview_session.getGraph(patient_id) + graph['connection']['glucoseMeasurement'] except requests.exceptions.ConnectionError: logging.error("failed to get details about patient, connection error") continue @@ -318,34 +239,21 @@ def main(): except requests.exceptions.RequestException: logging.error("failed to get details about patient an unknown error occurred") continue - - try: - graph['connection']['glucoseMeasurement'] except KeyError: logging.error("the data returned from the API was not in an expected format") continue glucoseMeasurement = graph['connection']['glucoseMeasurement'] - if show_in_terminal: - clear_terminal() - - print(f"Timestamp: {glucoseMeasurement['Timestamp']}\n") - print(f"Glucose units: {glucoseMeasurement['Value']}") - print(f"Is high: {glucoseMeasurement['isHigh']}") - print(f"Is low: {glucoseMeasurement['isLow']}") - print(f"Refresh rate: {delay} seconds") - - if show_on_screen: - clear_display() + now = datetime.now() + lines = [ + f"{now.strftime("%H:%M:%S")}", + str(glucoseMeasurement['Value']) + ] + display.displayLines(lines) - draw.text((x, top), glucoseMeasurement['Timestamp'], font=font, fill=255) - draw.text((x, top+8), "Glucose units: " + str(glucoseMeasurement['Value']), font=font, fill=255) - # Don't know why these two need to have more spaces to line up with the line ebovee - draw.text((x, top+16), "Is high: " + str(glucoseMeasurement['isHigh']), font=font, fill=255) - draw.text((x, top+24), "Is low: " + str(glucoseMeasurement['isLow']), font=font, fill=255) + time.sleep(delay) - display_on_screen(image) logging.debug("exiting now...") diff --git a/screens/__init__.py b/screens/__init__.py new file mode 100644 index 0000000..093751d --- /dev/null +++ b/screens/__init__.py @@ -0,0 +1,3 @@ +from .terminal_screen import TerminalScreen +from .debug_screens import DebugScreen +from .waveshare_233_ssd1305 import Waveshare_233_SSD1305 diff --git a/screens/debug_screens.py b/screens/debug_screens.py new file mode 100644 index 0000000..6352eb8 --- /dev/null +++ b/screens/debug_screens.py @@ -0,0 +1,65 @@ +import os +import logging +from typing import List + + +import psutil +from PIL import Image, ImageDraw, ImageFont + + +from .screen import Screen + + +class DebugScreen(Screen): + def __init__(self, width: int, height: int, number_of_colour_bits: str): + self.type = "Debug Screen" + + self.supports_custom_fonts = True + + self.width = width + self.height = height + self.image = Image.new(number_of_colour_bits, (self.width, self.height)) # Create a new image with 1 bit colour + self.draw = ImageDraw.Draw(self.image) + + def setDefaultFont(self): + logging.debug("setting font to default font") + ImageFont.load_default() + + def setFont(self, font: str, size: int): + logging.debug(f"setting font to {font} with size, {size}") + self.font = font + self.font_size = size + ImageFont.truetype(font, size) + + def clearBuffer(self): + self.draw.rectangle((0, 0, self.width, self.height), outline=0, fill=0) + + def displayOnScreen(self): + self.image.show() + + def displayLines(self, lines: List[str], x_padding: int = 0, y_padding: int = 4): + # displays text on the screen, the line that is first in the list is displayed on the top + self.clearBuffer() + self.clearScreen() + + x_start = 0 + y_start = 0 + for i, line in enumerate(lines): + self.draw.text((x_start+(i*x_padding), y_start+int(i*self.font_size)+(i*y_padding)), line, fill=255) + self.displayOnScreen() + + def clearScreen(self): + xdg_open_pid = -1 + for p in psutil.process_iter(): + if p.ppid() == os.getpid() and p.name() == "xdg-open": + logging.debug(f"found xdg-open process with a process id {p.pid}") + xdg_open_pid = p.pid + elif xdg_open_pid != -1 and p.ppid() == xdg_open_pid: + logging.debug(f"killing process, {p.name()} {p.cmdline()}") + p.terminate() + p.kill() + + self.clearBuffer() + draw = ImageDraw.Draw(self.image) + # black rectangle to clear screen, without clearing buffer + draw.rectangle((0, 0, self.width, self.height), outline=0, fill=0) diff --git a/drive/SSD1305.py b/screens/drive/SSD1305.py similarity index 100% rename from drive/SSD1305.py rename to screens/drive/SSD1305.py diff --git a/drive/config.py b/screens/drive/config.py similarity index 100% rename from drive/config.py rename to screens/drive/config.py diff --git a/screens/screen.py b/screens/screen.py new file mode 100644 index 0000000..5e2ae6b --- /dev/null +++ b/screens/screen.py @@ -0,0 +1,14 @@ +class Screen: + def __init__(self): + self.type = None + + self.supports_custom_fonts = False + + def getScreenType(self): + return self.type + + def clearScreen(self): + self.__clearScreen() + + def __clearScreen(self): + raise NotImplementedError("Subclass must implement this method") diff --git a/screens/terminal_screen.py b/screens/terminal_screen.py new file mode 100644 index 0000000..1794d07 --- /dev/null +++ b/screens/terminal_screen.py @@ -0,0 +1,28 @@ +from typing import List +import os + + +from .screen import Screen + + +class TerminalScreen(Screen): + def __init__(self): + self.type = "Terminal Screen" + + self.width, self.height = os.get_terminal_size() + + def displayLines(self, lines: List[str], x_padding: int = 0, y_padding: int = 4): + # displays text on the screen + self.clearScreen() + + y = 0 + x = 0 + for i, line in enumerate(lines): + # TODO: Calculate x and y before printing to check if we are going to go off the terminal + print('\n' * int(y+(y_padding*i)), end='') + print(' ' * int(x+(x_padding*i)), end='') + print(line, end='') + + + def clearScreen(self): + print("\033c", end='') diff --git a/screens/waveshare_233_ssd1305.py b/screens/waveshare_233_ssd1305.py new file mode 100644 index 0000000..55450f5 --- /dev/null +++ b/screens/waveshare_233_ssd1305.py @@ -0,0 +1,56 @@ +from typing import List + + +from PIL import Image, ImageDraw, ImageFont + + +from .screen import Screen + + +class Waveshare_233_SSD1305(Screen): + def __init__(self): + from drive import SSD1305 + + self.type = "Waveshare 2.33 SSD1305 OLED" + + self.supports_custom_fonts = True + + self.ssd1305 = SSD1305.SSD1305() + self.ssd1305.Init() + + self.width = self.ssd1305.width + self.height = self.ssd1305.height + self.image = Image.new('1', (self.width, self.height)) # Create new image with 1 bit colour + self.draw = ImageDraw.Draw(self.image) + + def setDefaultFont(self): + ImageFont.load_default() + + def setFont(self, font, size): + self.font = font + self.font_size = size + ImageFont.truetype(font, size) + + def clearBuffer(self): + self.draw.rectangle((0, 0, self.width, self.height), outline=0, fill=0) + + def displayOnScreen(self): + self.ssd1305.getbuffer(self.image) + self.ssd1305.ShowImage() + + def displayLines(self, lines: List[str]): + # displays text on the screen, the line that is first in the list is displayed on the top + y = 0 + x = 0 + for i, line in lines: + self.draw.text((x, y+(i*self.font_size)), line, font=self.font, fill=255) + self.displayOnScreen() + + def clearScreen(self): + # Used by parent class Screen + buffer = Image.new('1', (self.width, self.height)) + draw = ImageDraw.Draw(self.image) + # black rectangle to clear screen, without clearing buffer + draw.rectangle((0, 0, self.width, self.height), outline=0, fill=0) + self.ssd1305.getbuffer(buffer) + self.ssd1305.ShowImage()