From 07b3aca1972b66ea15c5ec51f2a05dc0a72227e0 Mon Sep 17 00:00:00 2001 From: avg-lebesgue-enjoyer Date: Fri, 6 Sep 2024 09:24:50 +1000 Subject: [PATCH] client control flow mostly works, but parts of the main loop are untested. Be warned. --- client/drivers/ai_bros.py | 70 ++++ client/drivers/data_structures.py | 284 ++++++++++--- client/drivers/font-pet-me-128.dat | Bin 0 -> 768 bytes client/drivers/main.py | 388 +++++++++++++----- client/proof_of_concept/__init__.py | 0 client/proof_of_concept/buttons.py | 112 +++++ client/proof_of_concept/camera.py | 125 ++++++ .../so-git-commits-this-folder.txt | 1 + client/proof_of_concept/cushion.py | 5 + client/proof_of_concept/display.py | 264 ++++++++++++ client/proof_of_concept/font-pet-me-128.dat | Bin 0 -> 768 bytes client/proof_of_concept/gpio.py | 56 +++ .../proof_of_concept/resources/hilarious.pbm | Bin 0 -> 1034 bytes .../resources/piicodev-logo.pbm | Bin 0 -> 1080 bytes client/proof_of_concept/servo_driver.py | 101 +++++ 15 files changed, 1253 insertions(+), 153 deletions(-) create mode 100644 client/drivers/ai_bros.py create mode 100644 client/drivers/font-pet-me-128.dat create mode 100644 client/proof_of_concept/__init__.py create mode 100644 client/proof_of_concept/buttons.py create mode 100644 client/proof_of_concept/camera.py create mode 100644 client/proof_of_concept/camera_dump/so-git-commits-this-folder.txt create mode 100644 client/proof_of_concept/cushion.py create mode 100644 client/proof_of_concept/display.py create mode 100644 client/proof_of_concept/font-pet-me-128.dat create mode 100644 client/proof_of_concept/gpio.py create mode 100644 client/proof_of_concept/resources/hilarious.pbm create mode 100644 client/proof_of_concept/resources/piicodev-logo.pbm create mode 100644 client/proof_of_concept/servo_driver.py diff --git a/client/drivers/ai_bros.py b/client/drivers/ai_bros.py new file mode 100644 index 0000000..5152f5a --- /dev/null +++ b/client/drivers/ai_bros.py @@ -0,0 +1,70 @@ +## SECTION: Imports + +from typing import Tuple +from PiicoDev_Switch import PiicoDev_Switch +from PiicoDev_SSD1306 import * +import threading +from datetime import datetime + +#from PiicoDev_Unified import sleep_ms + +from data_structures import ControlledData, HardwareComponents, Picture, Face + + +def ai_bros_face_recogniser(underlying_picture : "UNDERLYING_PICTURE") -> Face: # TODO: Refine type signature + """ + Recognise a face, powered by AI. + + Args: + underlying_picture : UNDERLYING_PICTURE + The picture to pass to the face recogniser. This data passing may be handled differently + in the final version. + Returns: + (Face): Failed, matched or unmatched Face + TODO: Convert this into an external API call. Currently returns debug data. + """ + # DEBUG: + print(" ai_bros_face_recogniser()") + DEBUG_failed = False + DEBUG_matched = True + DEBUG_user_id = -42 + # :DEBUG + if DEBUG_failed: + return Face.make_failed() + if not DEBUG_matched: + return Face.make_unmatched() + return Face.make_matched(DEBUG_user_id) + +def ai_bros_posture_score(underlying_picture : "UNDERLYING_PICTURE") -> int: # TODO: Refine type signature + """ + Args: + underlying_picture : UNDERLYING_PICTURE + The picture of the person's posture + Returns: + int: score represtning how good the posture currently is??? + TODO: Convert this into an external API call. Currently returns debug data. + NOTE: This will eventually be a database lookup. We're running the AI posture peeker + asynchronously to the controller code. + FIXME: This documentation is terrible + """ + return 1 + +def ai_bros_get_posture_data(last_snapshot_time : datetime) -> "POSTURE_DATA": # TODO: Refine type signature + """ + API call to get posture data from the SQLite database. + Gets all data from after last_snapshot_time until the current time. + + Args: + last_snapshot_time : datetime + The last time we read the posture data. + + Returns: + (POSTURE_DATA): Posture data returned from the API call. + + TODO: Actually implement this method. Currently prints a debug method and returns an empty list. + """ + # DEBUG: + print(" ai_bros_get_posture_data()") + DEBUG_return_value = [] + # :DEBUG + return DEBUG_return_value diff --git a/client/drivers/data_structures.py b/client/drivers/data_structures.py index cd29301..626c8d8 100644 --- a/client/drivers/data_structures.py +++ b/client/drivers/data_structures.py @@ -9,15 +9,22 @@ ## SECTION: Imports +from typing import List from PiicoDev_Switch import PiicoDev_Switch +from PiicoDev_SSD1306 import * from datetime import datetime +# DEBUG: +from math import sin, pi +# :DEBUG ## SECTION: Constants -EMPTY_USER_ID = None # TODO: Refine to a legal term +""" Sentinel value for an invalid user. """ +EMPTY_USER_ID = -1 + EMPTY_POSTURE_DATA = None # TODO: Refine to a legal term, once the type is figured out @@ -38,7 +45,7 @@ class ControlledData: self._posture_data : (TODO: figure out this type) Data updated through ML models, used for feedback. self._last_snapshot_time : datetime.datetime - Time of the last successful pose estimation. + Time of the last successful pull of posture data from the SQLite database self._last_cushion_time : datetime.datetime Time of the last successful cushion feedback event. self._last_plant_time : datetime.datetime @@ -63,6 +70,8 @@ def __init__(self): self._last_cushion_time = datetime.now() self._last_plant_time = datetime.now() self._last_sniff_time = datetime.now() + self._DEBUG_current_graph_list_index = 0 + self._DEBUG_current_graph_function = lambda x: 30 * (1 + sin(2 * pi * x / WIDTH)) @classmethod def make_empty(cls, user_id : int) -> "ControlledData": @@ -80,6 +89,7 @@ def make_empty(cls, user_id : int) -> "ControlledData": return_me._last_cushion_time = datetime.now() return_me._last_plant_time = datetime.now() return_me._last_sniff_time = datetime.now() + print(" Made a new empty ControlledData() with user_id", return_me._user_id) return return_me @classmethod @@ -102,6 +112,19 @@ def make_failed(cls) -> "ControlledData": # SECTION: Getters/Setters + def DEBUG_get_next_posture_graph_value(self) -> int: + """ + Get next thing to put on the DEBUG graph. + + Returns: + (int): Next thing to put on the DEBUG graph. + + TODO: Remove this method + """ + return_me = self._DEBUG_current_graph_function(self._DEBUG_current_graph_list_index) + self._DEBUG_current_graph_list_index += 1 + return return_me + def is_failed(self) -> bool: """ Returns: @@ -119,10 +142,85 @@ def get_user_id(self) -> int: def get_posture_data(self) -> "POSTURE_DATA": # TODO: Refine type signature """ Returns: - (POSTURE_DATA): The posture data stored in this ControlledData + (POSTURE_DATA): The posture data stored in this ControlledData. """ return self._posture_data + def get_last_snapshot_time(self) -> datetime: + """ + Returns: + (datetime): The last time that the internal posture data was updated. + """ + return self._last_snapshot_time + + def set_last_snapshot_time(self, time : datetime) -> None: + """ + Args: + time : datetime + The last time that the internal posture data was updated. + """ + self._last_snapshot_time = time + + def get_last_cushion_time(self) -> datetime: + """ + Returns: + (datetime): The last time that the user was provided cushion feedback. + """ + return self._last_cushion_time + + def set_last_cushion_time(self, time : datetime) -> None: + """ + Args: + time : datetime + The last time that the user was provided cushion feedback. + """ + self._last_cushion_time = time + + def get_last_plant_time(self) -> datetime: + """ + Returns: + (datetime): The last time that the user was provided plant feedback. + """ + return self._last_plant_time + + def set_last_plant_time(self, time : datetime) -> None: + """ + Args: + time : datetime + The last time that the user was provided plant feedback. + """ + self._last_plant_time = time + + def get_last_sniff_time(self) -> datetime: + """ + Returns: + (datetime): The last time that the user was provided olfactory feedback. + """ + return self._last_cushion_time + + def set_last_sniff_time(self, time : datetime) -> None: + """ + Args: + time : datetime + The last time that the user was provided olfactory feedback. + """ + self._last_sniff_time = time + + def accept_new_posture_data(self, posture_data : "POSTURE_DATA") -> None: # TODO: Refine type signature + """ + Update the internal store of posture data. + + Args: + posture_data : POSTURE_DATA + New posture data to accept and merge with the current state of this object. + + TODO: Implement me! + """ + # DEBUG: + print(" accept_new_posture_data()") + # :DEBUG + + # SECTION: Posture data mapping def get_cushion_posture_data(self) -> "CUSHION_POSTURE_DATA": # TODO: Decide what this type looks like @@ -167,8 +265,15 @@ class HardwareComponents: Hardware components packaged together into a class. Member fields: - self._buttons : [PiicoDev_Switch] - A list of individual buttons. Will have length TWO (2). + self.button0 : PiicoDev_Switch + A button with address switches set to [0, 0, 0, 0] + self.button1 : PiicoDev_Switch + A button with address switches set to [0, 0, 0, 1] + self.display : PiicoDev_SSD1306 + OLED SSD1306 Display with default address + self.posture_graph : PiicoDev_SSD1306.graph2D | None + Graph object for rendering on self.display. NOT INITIALISED by default; i.e. None until initialised. + Should get initialised ONCE THE USER IS LOGGED IN because the graph will look different for each user. """ # SECTION: Constructors @@ -181,26 +286,113 @@ def make_fresh(cls): """ DOUBLE_PRESS_DURATION = 400 # Milliseconds return HardwareComponents( - PiicoDev_Switch(id = [0, 0, 0, 0], double_press_duration = DOUBLE_PRESS_DURATION), - PiicoDev_Switch(id = [0, 0, 0, 1], double_press_duration = DOUBLE_PRESS_DURATION) + PiicoDev_Switch(id = [0, 0, 0, 0], double_press_duration = DOUBLE_PRESS_DURATION), # WARNING: 2024-09-01 17:12 Gabe: I think this produces an "I2C is not enabled" warning. No idea why. + PiicoDev_Switch(id = [0, 0, 0, 1], double_press_duration = DOUBLE_PRESS_DURATION), # WARNING: 2024-09-01 17:12 Gabe: I think this produces an "I2C is not enabled" warning. No idea why. + create_PiicoDev_SSD1306() # This is the constructor; ignore the "is not defined" error message. ) - def __init__(self, button0, button1): - self._buttons = [button0, button1] + def __init__(self, button0, button1, display): + self.button0: PiicoDev_Switch = button0 + self.button1: PiicoDev_Switch = button1 + self.display: PiicoDev_SSD1306 = display + self.posture_graph : PiicoDev_SSD1306.graph2D | None = None + + # SECTION: Setters + + # 2024-09-02 14-45 Gabe: TESTED. + def get_control_messages(self, user_id : int) -> List[int]: + """ + Get messages to display during usual application loop. + TODO: Finalise these! + + Args: + user_id : int + ID of the currently logged-in user. + """ + return ["b0: logout", "id: " + str(user_id)] + + def initialise_posture_graph(self, user_id : int) -> None: + """ + Initialise self.posture_graph according to the provided user_id. + + Args: + user_id : int + ID of the currently logged-in user. + + WARNING: UNTESTED! + """ + CONTROL_MESSAGES = self.get_control_messages(user_id) + GRAPH_MIN_VALUE = 0 + GRAPH_MAX_VALUE = 60 # TODO: 2024-09-02 07-53 Gabe: + # This needs to be a real value for the underlying data that we expect to be shown. + # From memory, this is probably `60` for "number of the last 60 seconds spent sitting well" + LINE_HEIGHT = 15 # pixels + LINE_WIDTH = 16 # characters + + # The posture graph will occupy space from the bottom (y = HEIGHT - 1) up to initialisation_y_value. + initialisation_y_value = len([CONTROL_MESSAGES[i : i + LINE_WIDTH] for i in range(0, len(CONTROL_MESSAGES), LINE_WIDTH)]) * LINE_HEIGHT + self.posture_graph = self.display.graph2D( + originX = 0, originY = HEIGHT - 1, + width = WIDTH, height = HEIGHT - initialisation_y_value, + minValue = GRAPH_MIN_VALUE, maxValue = GRAPH_MAX_VALUE, + c = 1, bars = False + ) - # SECTION: Getters + # SECTION: Using peripherals + + # 2024-09-01 16:57 Gabe: TESTED. + def oled_display_text(self, text : str, x : int, y : int, colour : int) -> int: + """ + Display text on the oled display, wrapping lines if necessary. + NOTE: Does not blank display. Call `.display.fill(0)` if needed. + NOTE: Does not render. Call `.display.show()` if needed. - def get_button(self, index: int) -> PiicoDev_Switch: + Args: + text : str + String to write to the OLED display. + x : int + Horizontal coordinate from left side of screen. + y : int + Vertical coordinate from top side of screen. + colour : int + 0: black + 1: white + + Returns: + (int): The y-value at which any subsequent lines should start printing from. + """ + LINE_HEIGHT = 15 # pixels + LINE_WIDTH = 16 # characters + chunks = [text[i : i + LINE_WIDTH] for i in range(0, len(text), LINE_WIDTH)] + for (index, chunk) in enumerate(chunks): + self.display.text(chunk, x, y + index * LINE_HEIGHT, colour) + return y + len(chunks) * LINE_HEIGHT + + # 2024-09-01 17:12 Gabe: TESTED. + def oled_display_texts(self, texts: List[str], x : int, y : int, colour : int) -> int: """ - Get a button. The index determines which button. + Display many lines of text on the oled display, wrapping lines if necessary. + NOTE: Does not blank display. Call `.display.fill(0)` if needed. + NOTE: Does not render. Call `.display.show()` if needed. Args: - index : int - The button to select + texts : List[str] + Strings to write to the OLED display. Each string begins on a new line. + x : int + Horizontal coordinate from left side of screen. + y : int + Vertical coordinate from top side of screen. + colour : int + 0: black + 1: white + Returns: - (PiicoDev_Switch): The button object selected + (int): The y-value at which any subsequent lines should start printing from. """ - return self._buttons[index] + display_height_offset = 0 + for text in texts: + display_height_offset = self.oled_display_text(text, x, y + display_height_offset, colour) + return display_height_offset @@ -211,15 +403,15 @@ class Picture: A picture, which may have failed. Member fields: - self._failed : bool + self.failed : bool True iff this picture is incomplete. - self._underlying_picture : (TODO: Figure out this type!) + self.underlying_picture : (TODO: Figure out this type!) The picture encoded by this object. """ def __init__(self) -> "Picture": - self._failed = True - self._underlying_picture = None + self.failed : bool = True + self.underlying_picture : "UNDERLYING_PICTURE" = None @classmethod def make_failed(cls) -> "Picture": @@ -227,8 +419,8 @@ def make_failed(cls) -> "Picture": Make a failed Picture. """ return_me = Picture() - return_me._failed = True - return_me._underlying_picture = None + return_me.failed = True + return_me.underlying_picture = None return return_me @classmethod @@ -237,22 +429,10 @@ def make_valid(cls, underlying_picture : "UNDERLYING_PICTURE") -> "Picture": Make a valid Picture. """ return_me = Picture() - return_me._failed = False - return_me._underlying_picture = underlying_picture + return_me.failed = False + return_me.underlying_picture = underlying_picture return return_me - def is_failed(self) -> bool: - """ - Check whether this object is failed. - """ - return self._failed - - def get_underlying_picture(self) -> "UNDERLYING_PICTURE": - """ - Get the underlying picture in this object. - """ - return self._underlying_picture - ## SECTION: Face @@ -262,19 +442,19 @@ class Face: A potentially recognised face, which may have failed to match or failed to run. Member fields: - self._failed : bool + self.failed : bool True iff the facial recognition model failed - self._matched : bool + self.matched : bool True iff the facial recognition model matched a face - self._user_id : int - if not self._failed and self._matched, then this string will contain the user + self.user_id : int + if not self.failed and self.matched, then this string will contain the user id of the matched user """ def __init__(self) -> "Face": - self._failed = True - self._matched = False - self._user_id = None + self.failed : bool = True + self.matched : bool = False + self.user_id : int = None @classmethod def make_failed(cls) -> "Face": @@ -282,9 +462,9 @@ def make_failed(cls) -> "Face": Make a failed Face. """ return_me = Face() - return_me._failed = True - return_me._matched = False - return_me._user_id = None + return_me.failed = True + return_me.matched = False + return_me.user_id = None return return_me @classmethod @@ -293,9 +473,9 @@ def make_unmatched(cls) -> "Face": Make an unmatched Face. """ return_me = Face() - return_me._failed = False - return_me._matched = False - return_me._user_id = None + return_me.failed = False + return_me.matched = False + return_me.user_id = None return return_me @classmethod @@ -308,7 +488,7 @@ def make_matched(cls, user_id : int) -> "Face": The matched user id """ return_me = Face() - return_me._failed = False - return_me._matched = True - return_me._user_id = user_id + return_me.failed = False + return_me.matched = True + return_me.user_id = user_id return return_me diff --git a/client/drivers/font-pet-me-128.dat b/client/drivers/font-pet-me-128.dat new file mode 100644 index 0000000000000000000000000000000000000000..ad4d9e3d0514af3fb3010ed1d35af26321291aec GIT binary patch literal 768 zcmX|9O>5gg5FJ}WUK3H5Vu}i)af4_}4nd?~oFMc;<+g$vH=sfTrYMnV4>5$c2D@bK z@z3a?ha7V1Z^$7(rHB3um-LMsI%4b(Wpk7xh;ddy|c5!2Snuy z$Np3j?ekzT`}z)%re)LsZdfLgn3SPQq;^Bs4R;kO6^2T|zxrLsv8qf~MIm!s_>OJW zDdeorA@(VA921>>JIaQyh<3^><1Ufov>!6xr)Qu0{lx)Ee0H%LUT$qIH4)#{gFh*EWWQ-l z{8ERx*{q>&oM#t@vR8^3LvB&svRH_#)Kdh-f=1ifCKne8)$ja^|`-+)JZ1pSN&#Ctr>SuU|q4S|s6FVmDfqQyey_-3{Gixu?sCMg`CjR9IazN0m86EF3JXTA2kz`;ESp0{30oM}QI zy&2{Cv_qyDaXRi01v-v(faewzMc@K8`qwU{lU~L1^ZN2q^rYkI=q-5h{$|w=pxax} Sqy1U+9_vQU!1(ok^Zf(-nxWPJ literal 0 HcmV?d00001 diff --git a/client/drivers/main.py b/client/drivers/main.py index 2fd4f44..31bf1e0 100644 --- a/client/drivers/main.py +++ b/client/drivers/main.py @@ -9,10 +9,45 @@ ## SECTION: Imports -from PiicoDev_Switch import PiicoDev_Switch -#from PiicoDev_Unified import sleep_ms +from PiicoDev_Unified import sleep_ms +from PiicoDev_Switch import * +from PiicoDev_SSD1306 import * + +from typing import Tuple +import threading +from datetime import datetime from data_structures import ControlledData, HardwareComponents, Picture, Face +from ai_bros import * + + + +## SECTION: Global constants + +""" Number of milliseconds between each time the button is polled during wait_for_login_attempt(). """ +WAIT_FOR_LOGIN_POLLING_INTERVAL = 100 +""" Number of milliseconds between pictures taken for login attempts. """ +LOGIN_TAKE_PICTURE_INTERVAL = 1000 +""" Number of milliseconds between starting to attempt_login() and taking the first picture. """ +START_LOGIN_ATTEMPTS_DELAY = 3000 +""" Number of milliseconds between each time the button is polled during ask_create_new_user(). """ +ASK_CREATE_NEW_USER_POLLING_INTERVAL = 100 +""" Number of milliseconds between telling the user that login has completely failed and returning from attempt_login(). """ +FAIL_LOGIN_DELAY = 3000 +""" Number of milliseconds between telling the user that login has succeeded and beginning real functionality. """ +LOGIN_SUCCESS_DELAY = 3000 +""" Number of milliseconds between the user successfully logging out and returning to main(). """ +LOGOUT_SUCCESS_DELAY = 3000 +""" Minimum delay between reading posture data from the SQLite database, in do_everything(). """ +GET_POSTURE_DATA_TIMEOUT = 2000 # DEBUG: Change this value up to ~60000 later. +""" Minimum delay between consecutive uses of the vibration motor. Used in handle_feedback(). """ +HANDLE_CUSHION_FEEDBACK_TIMEOUT = 5000 +""" Minimum delay between consecutive uses of the plant-controlling servos. Used in handle_feedback(). """ +HANDLE_PLANT_FEEDBACK_TIMEOUT = 10000 +""" Minimum delay between consecutive uses of the scent bottle-controlling servos. Used in handle_feedback(). """ +HANDLE_SNIFF_FEEDBACK_TIMEOUT = 20000 +""" DEBUG Number of milliseconds between each loop iteration in do_everything(). """ +DEBUG_DO_EVERYTHING_INTERVAL = 1000 @@ -26,21 +61,24 @@ def main(): print(" main()") # :DEBUG + global hardware hardware = initialise_hardware() # Top level control flow while True: - wait_for_login_attempt(hardware.get_button(0)) + wait_for_login_attempt() main_data = attempt_login() if main_data.is_failed(): continue + print(" main(): Successful login") do_everything(main_data) ## SECTION: Hardware initialisation -def initialise_hardware(): +# 2024-09-01_15-29 Gabe: TESTED. for buttons and OLED display. +def initialise_hardware() -> HardwareComponents: """ Set up hardware for use throughout the project. @@ -48,18 +86,21 @@ def initialise_hardware(): (HardwareComponents): Object consisting of all hardware components connected to the Raspberry Pi. TODO: Complete the function with all of the hardware peripherals (incrementally, as they get integrated). - WARNING: UNTESTED! """ - # DEBUG: - print(" initialise_hardware()") - # :DEBUG - return HardwareComponents.make_fresh() + print(" initialise_hardware()") # DEBUG + return_me = HardwareComponents.make_fresh() + # Clear button queues + return_me.button0.was_pressed + return_me.button1.was_pressed + print(" initialise_hardware() FINISHED") # DEBUG + return return_me ## SECTION: Login handling -def wait_for_login_attempt(button0 : PiicoDev_Switch) -> bool: +# 2024-09-01_15-52 Gabe: TESTED. +def wait_for_login_attempt() -> bool: """ Waits until the user attempts to log in. @@ -67,19 +108,28 @@ def wait_for_login_attempt(button0 : PiicoDev_Switch) -> bool: button0 : PiicoDev_Switch Button to wait for press on Returns: - (bool): True when the user attempts to log in. - - WARNING: UNTESTED! + (bool): True when the user attempts to log in. """ - # DEBUG: print(" BEGIN wait_for_login_attempt()") - # :DEBUG + + WAIT_FOR_LOGIN_OLED_MESSAGE = "Press button0 to log in!" + # Display to screen + hardware.display.fill(0) + hardware.oled_display_text(WAIT_FOR_LOGIN_OLED_MESSAGE, 0, 0, 1) + hardware.display.show() + # Clear button queue + hardware.button0.was_pressed while True: - if button0.was_pressed: + if hardware.button0.was_pressed: + # Clear the display + hardware.display.fill(0) + hardware.display.show() print(" END wait_for_login_attempt()") # DEBUG return True + sleep_ms(WAIT_FOR_LOGIN_POLLING_INTERVAL) +# 2024-09-01 17:06 Gabe: TESTED., assuming ai_bros_face_recogniser() does what it should do. def attempt_login() -> ControlledData: """ Attempts to log in. @@ -88,33 +138,51 @@ def attempt_login() -> ControlledData: (ControlledData): which is: FAILED if the login is unsuccessful EMPTY (but not failed) if the login is successful - - TODO: Actually write this function. Currently prints a debug message. """ - # # DEBUG: - # print(" attempt_login()") - # DEBUG_login_success = True - # DEBUG_default_user_id = "play-user" - # # :DEBUG - # if DEBUG_login_success: - # return ControlledData.make_empty(DEBUG_default_user_id) - # else: - # return ControlledData.make_failed() - + + # TODO: Finalise these messages + SMILE_FOR_CAMERA_MESSAGE = "LIS: Smile for the camera!" + PICTURE_FAILED_MESSAGE = "LIS: Picture failed T-T" + AI_FAILED_MESSAGE = "LIS: Failed to determine user T-T" + LOGIN_TOTALLY_FAILED_MESSAGE = "LIS: Failed to log in T-T" + + hardware.display.fill(0) + hardware.oled_display_text(SMILE_FOR_CAMERA_MESSAGE, 0, 0, 1) + hardware.display.show() + sleep_ms(START_LOGIN_ATTEMPTS_DELAY) + while True: + hardware.display.fill(0) + hardware.oled_display_text(SMILE_FOR_CAMERA_MESSAGE, 0, 0, 1) + hardware.display.show() picture = take_picture() - if picture.is_failed(): + if picture.failed: + print(' Picture Failed') # DEBUG + hardware.display.fill(0) + hardware.oled_display_text(PICTURE_FAILED_MESSAGE, 0, 0, 1) + hardware.display.show() + sleep_ms(LOGIN_TAKE_PICTURE_INTERVAL) continue face = ai_bros_face_recogniser(picture.underlying_picture) # TODO: This should be an external API call. - if face.is_failed(): + if face.failed: + print(" AI has failed us") # DEBUG + hardware.display.fill(0) + hardware.oled_display_text(AI_FAILED_MESSAGE, 0, 0, 1) + hardware.display.show() + sleep_ms(LOGIN_TAKE_PICTURE_INTERVAL) continue - if face.is_matched(): - return ControlledData.make_empty(face.get_user_id()) - if not ask_create_new_user(): - continue - new_user_id = create_new_user(picture.underlying_picture) - return ControlledData.make_empty(new_user_id) - + if face.matched: + print(" Mega W for AI") # DEBUG + return ControlledData.make_empty(face.user_id) + elif ask_create_new_user(): + return ControlledData.make_empty(create_new_user(picture)) + # Tell the user the login failed + print(" attempt_login(): Totally failed lol") # DEBUG + hardware.display.fill(0) + hardware.oled_display_text(LOGIN_TOTALLY_FAILED_MESSAGE, 0, 0, 1) + hardware.display.show() + sleep_ms(FAIL_LOGIN_DELAY) + return ControlledData.make_failed() def take_picture() -> Picture: """ @@ -124,34 +192,11 @@ def take_picture() -> Picture: """ # DEBUG: print(" take_picture()") - DEBUG_return_value = Picture.make_failed() + DEBUG_return_value = Picture.make_valid("DEBUG_picture_goes_here") # :DEBUG return DEBUG_return_value -def ai_bros_face_recogniser(underlying_picture : "UNDERLYING_PICTURE") -> Face: - """ - Recognise a face, powered by AI. - - Args: - underlying_picture : UNDERLYING_PICTURE - The picture to pass to the face recogniser. This data passing may be handled differently - in the final version. - Returns: - (Face): Failed, matched or unmatched Face - TODO: Convert this into an external API call. Currently returns debug data. - """ - # DEBUG: - print(" ai_bros_face_recogniser()") - DEBUG_failed = False - DEBUG_matched = True - DEBUG_user_id = 0 - # :DEBUG - if DEBUG_failed: - return Face.make_failed() - if not DEBUG_matched: - return Face.make_unmatched() - return Face.make_matched(DEBUG_user_id) - +# 2024-09-01 17:06 Gabe: TESTED. def ask_create_new_user() -> bool: """ Ask the user whether they would like to create a new user profile based on the previous picture. @@ -162,10 +207,31 @@ def ask_create_new_user() -> bool: Two buttons (yes / no) The LED display ("Unmatched face. Create new user?") """ - # DEBUG: - DEBUG_user_response = False - # :DEBUG - return DEBUG_user_response + print(" BEGIN ask_create_new_user()") + + CREATE_NEW_USER_MESSAGES = ["No face matched.", "Create new user?", "button0: no", "button1: yes"] + # Display to screen + hardware.display.fill(0) + hardware.oled_display_texts(CREATE_NEW_USER_MESSAGES, 0, 0, 1) + hardware.display.show() + # Clear button queue + hardware.button0.was_pressed + hardware.button1.was_pressed + + while True: + if hardware.button0.was_pressed: + # Clear the display + hardware.display.fill(0) + hardware.display.show() + print(" END ask_create_new_user(): do NOT create new user") # DEBUG + return False + if hardware.button1.was_pressed: + # Clear the display + hardware.display.fill(0) + hardware.display.show() + print(" END ask_create_new_user(): DO create new user") # DEBUG + return True + sleep_ms(ASK_CREATE_NEW_USER_POLLING_INTERVAL) def create_new_user(underlying_picture : "UNDERLYING_PICTURE") -> int: """ @@ -187,90 +253,210 @@ def create_new_user(underlying_picture : "UNDERLYING_PICTURE") -> int: ## SECTION: Control for the logged-in user -def do_everything(uqcs : ControlledData) -> None: +# 2024-09-02 07-03 Gabe: Currently working the following here: +# top-level control flow +# interaction with buttons and display +def do_everything(auspost : ControlledData) -> None: """ Main control flow once a user is logged in. Args: - (uqcs : ControlledData): Data encapsulating the current state of the program. + (auspost : ControlledData): Data encapsulating the current state of the program. Requires: - ! uqcs.is_failed() + ! auspost.is_failed() TODO: Actually implement this """ - # DEBUG: - DEBUG_user_wants_to_log_out = False - # :DEBUG + print(" BEGIN do_everything()") + + LOGIN_MESSAGE = "Logged in with user id: " + str(auspost.get_user_id()) + LOGOUT_MESSAGE = "Logged out user id " + str(auspost.get_user_id()) + + # Initialise posture graph for the current session + hardware.initialise_posture_graph(auspost.get_user_id()) + + # Display message to user + hardware.display.fill(0) + hardware.oled_display_text(LOGIN_MESSAGE, 0, 0, 1) + hardware.display.show() + sleep_ms(LOGIN_SUCCESS_DELAY) + + # Clear button queues + hardware.button0.was_pressed + hardware.button1.was_pressed + while True: - # Loop invariant: ! uqcs.is_failed() - if DEBUG_user_wants_to_log_out: - return - update_display_screen(uqcs) - handle_posture_monitoring(uqcs) - handle_feedback(uqcs) - -def update_display_screen(uqcs : ControlledData) -> bool: + # Loop invariant: ! auspost.is_failed() + # Check for user actions + if hardware.button0.was_pressed: + hardware.display.fill(0) + hardware.oled_display_text(LOGOUT_MESSAGE, 0, 0, 1) + hardware.display.show() + sleep_ms(LOGOUT_SUCCESS_DELAY) + print(" END do_everything()") + return + + # Probably should run individual threads for each of these + # TODO: Move the threading to a more reasonable location. main() is probably best. + # posture_monitoring_thread = threading.Thread(handle_posture_monitoring, args=(auspost)) + # posture_monitoring_thread.start() + + # DEBUG: + update_display_screen(auspost) + handle_posture_monitoring(auspost) + handle_feedback(auspost) + # :DEBUG + + sleep_ms(DEBUG_DO_EVERYTHING_INTERVAL) + +def update_display_screen(auspost : ControlledData) -> bool: """ Update the display screen with whatever needs to be on there. - TODO: Determine what needs to be on there. + We will display: + As per HardwareComponents.get_control_messages(), and + Current-session posture graph Args: - (uqcs : ControlledData): + (auspost : ControlledData): Data encapsulating the current state of the program. Returns: (bool): True, always. If you get a False return value, then something has gone VERY wrong. Requires: - ! uqcs.is_failed() + ! auspost.is_failed() Ensures: - ! uqcs.is_failed() + ! auspost.is_failed() - TODO: Implement this method. Currently prints a debug statement. + WARNING: UNTESTED! """ - # DEBUG: - print(" update_display_screen()") - # :DEBUG + print(" BEGIN update_display_screen()") + + hardware.display.fill(0) + hardware.oled_display_texts(hardware.get_control_messages(auspost.get_user_id()), 0, 0, 1) + hardware.display.updateGraph2D(hardware.posture_graph, auspost.DEBUG_get_next_posture_graph_value()) + hardware.display.show() + + print(" END update_display_screen()") return True -def handle_posture_monitoring(uqcs : ControlledData) -> bool: +def handle_posture_monitoring(auspost : ControlledData) -> bool: """ Take a snapshot monitoring the user, and update the given ControlledData if necessary. Args: - (uqcs : ControlledData): Data encapsulating the current state of the program. + (auspost : ControlledData): Data encapsulating the current state of the program. Returns: (bool): True, always. If you get a False return value, then something has gone VERY wrong. Requires: - ! uqcs.is_failed() + ! auspost.is_failed() Ensures: - ! uqcs.is_failed() + ! auspost.is_failed() - TODO: Implement this method. Currently prints a debug statement. + TODO: Implement error handling + WARNING: UNTESTED! """ # DEBUG: print(" handle_posture_monitoring()") # :DEBUG - # See Control_flow.pdf for expected control flow + now = datetime.now() + if (now > auspost.get_last_snapshot_time() + GET_POSTURE_DATA_TIMEOUT): + # TODO: The ai_bros_get_posture_data() call might fail once it's implemented properly. + # If it does, we need to handle it properly. + auspost.accept_new_posture_data(ai_bros_get_posture_data(auspost.get_last_snapshot_time())) + auspost.set_last_snapshot_time(now) return True -def handle_feedback(uqcs : ControlledData) -> bool: +def handle_feedback(auspost : ControlledData) -> bool: """ Provide feedback to the user if necessary. Args: - (uqcs : ControlledData): Data encapsulating the current state of the program. + (auspost : ControlledData): Data encapsulating the current state of the program. + Returns: + (bool): True, always. If you get a False return value, then something has gone VERY wrong. + Requires: + ! auspost.is_failed() + Ensures: + ! auspost.is_failed() + """ + if (datetime.now() > auspost.get_last_cushion_time() + HANDLE_CUSHION_FEEDBACK_TIMEOUT): + if not handle_cushion_feedback(auspost): + return False + if (datetime.now() > auspost.get_last_plant_time() + HANDLE_PLANT_FEEDBACK_TIMEOUT): + if not handle_plant_feedback(auspost): + return False + if (datetime.now() > auspost.get_last_sniff_time() + HANDLE_SNIFF_FEEDBACK_TIMEOUT): + if not handle_sniff_feedback(auspost): + return False + + return True + + + +## SECTION: Feedback handling + +def handle_cushion_feedback(auspost : ControlledData) -> bool: + """ + Vibrate cushion (if necessary), and update the timestamp of when cushion feedback was last given. + + Args: + (auspost : ControlledData): Data encapsulating the current state of the program. + Returns: + (bool): True, always. If you get a False return value, then something has gone VERY wrong. + Requires: + ! auspost.is_failed() + Ensures: + ! auspost.is_failed() + + TODO: Implement this method. Currently prints a debug statement and updates the time. + """ + # DEBUG: + print(" handle_cushion_feedback()") + # :DEBUG + auspost.set_last_cushion_time(datetime.now()) + return True + +def handle_plant_feedback(auspost : ControlledData) -> bool: + """ + Set the plant height according to short-term current session data, and update the timestamp + of when plant feedback was last given. + + Args: + (auspost : ControlledData): Data encapsulating the current state of the program. + Returns: + (bool): True, always. If you get a False return value, then something has gone VERY wrong. + Requires: + ! auspost.is_failed() + Ensures: + ! auspost.is_failed() + + TODO: Implement this method. Currently prints a debug statement and updates the time. + """ + # DEBUG: + print(" handle_plant_feedback()") + # :DEBUG + auspost.set_last_plant_time(datetime.now()) + return True + +def handle_sniff_feedback(auspost : ControlledData) -> bool: + """ + Dispense olfactory reward (if necessary), and update the timestamp of when olfactory feedback + was last given. + + Args: + (auspost : ControlledData): Data encapsulating the current state of the program. Returns: (bool): True, always. If you get a False return value, then something has gone VERY wrong. Requires: - ! uqcs.is_failed() + ! auspost.is_failed() Ensures: - ! uqcs.is_failed() + ! auspost.is_failed() - TODO: Implement this method. Currently prints a debug statement. + TODO: Implement this method. Currently prints a debug statement and updates the time. """ # DEBUG: - print(" handle_feedback()") + print(" handle_sniff_feedback()") # :DEBUG - # See Control_flow.pdf for expected control flow + auspost.set_last_sniff_time(datetime.now()) return True diff --git a/client/proof_of_concept/__init__.py b/client/proof_of_concept/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/client/proof_of_concept/buttons.py b/client/proof_of_concept/buttons.py new file mode 100644 index 0000000..c31dbab --- /dev/null +++ b/client/proof_of_concept/buttons.py @@ -0,0 +1,112 @@ +""" +File: + sitting-desktop-garden/client/proof_of_concept/buttons.py + +Purpose: + Hardware test for the PiicoDev buttons. + +Author: + Gabriel Field (47484306) + +Source: + Largely based on https://core-electronics.com.au/guides/piicodev-button-getting-started-guide/#B62QRRH +""" + +## SECTION: Imports + +from PiicoDev_Switch import PiicoDev_Switch # Switch may be used for other types of PiicoDev Switch devices +from PiicoDev_Unified import sleep_ms + + + +## SECTION: main() + +def main(): + print("==== TEST: Chungus on one button ====") + button = PiicoDev_Switch(double_press_duration = 400) # Double-presses are those that happen with interval <= 400ms + basic_press_test(button) + led_test(button) + double_press_test(button) + + print(" Set button IDs to 0000 and 1110.") + print(" Do this by setting switches down for '0' and up for '1'.") + print(" You have 10 seconds...") + sleep_ms(1000 * 10) + print(" Assuming the button IDs have been set.") + + print("==== TEST: Chungi on two buttons ====") + button0000 = PiicoDev_Switch(id = [0, 0, 0, 0]) + button1110 = PiicoDev_Switch(id = [1, 1, 1, 0]) + two_chungi(button0000, button1110) + + print("==== TEST: COMPLETE! ====") + + + +## TEST: One-button + +def basic_press_test(button: PiicoDev_Switch): + print(" BEGIN basic_press_test()") + sleep_ms(2000) + cycle = 0 + press_count = 0 + while cycle < 10: + print("\tCycle: " + str(cycle)) + press_count += button.press_count + print("\t\tWas pressed: " + str(button.was_pressed)) + print("\t\tIs pressed: " + str(button.is_pressed)) + print("\t\tTotal press count: " + str(press_count)) + cycle += 1 + sleep_ms(1000) + print(" END basic_press_test()") + +def led_test(button): + print(" BEGIN led_test()") + sleep_ms(2000) + cycle = 0 + button.led = False + thinking_led = False + while cycle < 10: + print("\tCycle: " + str(cycle)) + button.led = not button.led + thinking_led = False + print("\tButton should be on: " + str(thinking_led)) + cycle += 1 + sleep_ms(1000) + print(" END led_test()") + +def double_press_test(button): + print(" BEGIN double_press_test()") + sleep_ms(2000) + cycle = 0 + while cycle < 10: + print("\tCycle: " + str(cycle)) + print("\tWas double-pressed: " + str(button.was_double_pressed)) + cycle += 1 + sleep_ms(1000) + print(" END double_press_test()") + + + +## TEST: Two buttons + +def two_chungi(button0000, button1110): + print(" BEGIN two_chungi()") + sleep_ms(2000) + cycle = 0 + button0000.press_count # Clear the count + button1110.press_count # Clear the count + while cycle < 10: + print("\tCycle: " + str(cycle)) + print("\tButton with id 0000 pressed this many times since last time: " + str(button0000.press_count)) + print("\tButton with id 1110 pressed this many times since last time: " + str(button1110.press_count)) + cycle += 1 + sleep_ms(1000) + print(" END two_chungi()") + + + +## LAUNCH + +if __name__ == "__main__": + main() diff --git a/client/proof_of_concept/camera.py b/client/proof_of_concept/camera.py new file mode 100644 index 0000000..ece4377 --- /dev/null +++ b/client/proof_of_concept/camera.py @@ -0,0 +1,125 @@ +""" +File: + sitting-desktop-garden/client/proof_of_concept/buttons.py + +Purpose: + Hardware test for the PiicoDev buttons. + +Author: + Gabriel Field (47484306) + +Source: + Largely based on https://projects.raspberrypi.org/en/projects/getting-started-with-picamera/0 +""" + +## SECTION: Imports + +from picamera2 import Picamera2, Preview +from PiicoDev_Unified import sleep_ms +from time import time + + + +## SECTION: main() + +def main(): + print("==== TEST: Preview ====") + picam2 = Picamera2() + test_preview(picam2) + + print("==== TEST: Pics ====") + test_pics(picam2) + + print("==== TEST: Vids ====") + test_vids(picam2) + + print("==== TEST: COMPLETE! ====") + + + +## TEST: Preview + +def test_preview(picam2): + print(" BEGIN test_preview()") + sleep_ms(2000) + # Preview for 10 seconds + print("\tStarting preview for 10 seconds... Requires monitor!!") + picam2.start_preview(Preview.QTGL) + picam2.start() + sleep_ms(10000) + picam2.close() + print(" END test_preview()") + + + +## TEST: Pics + +def test_pics(picam2): + print(" BEGIN test_pics()") + sleep_ms(2000) + # Take a single picture + print("\tTaking a picture to ./camera_dump/picture-00.jpg") + start = time() + picam2.start_and_capture_file("./camera_dump/picture-00.jpg") + picam2.close() + end = time() + print("\t\t^done. Took " + str(end - start) + " seconds") + sleep_ms(5000) + # Take many pictures + print("\tTaking 10 pictures to ./camera_dump-pictures-?.jpg for ? = 0 .. 9, with delay 0.1 seconds") + start = time() + picam2.start_and_capture_files("./camera_dump/pictures-{:d}.jpg", num_files = 10, delay = 0.1) + picam2.close() + end = time() + print("\t\t^done. Took " + str(end - start) + " seconds") + sleep_ms(5000) + print(" END test_pics()") + + + +## TEST: Vids + +def test_vids(picam2): + print(" BEGIN test_vids()") + sleep_ms(2000) + # Take a single video for 10 seconds + print("\tTaking a 10-second video to ./camera_dump/video-00.jpg") + start = time() + picam2.start_and_record_video("./camera_dump/video-00.jpg", duration = 10, show_preview = False) + picam2.close() + end = time() + print("\t\t^done. Took " + str(end - start) + " seconds") + sleep_ms(5000) + # Taking many short videos + print("\tTaking ten 1-second videos to ./camera_dump/shorts-?.jpg for ? = 0 .. 9") + start = time() + for i in range(10): + picam2.start_and_record_video("./camera_dump/shorts-" + str(i) + ".jpg", duration = 1, show_preview = False) + picam2.close() + end = time() + print("\t\t^done. Took " + str(end - start) + " seconds") + # Taking many tiny videos + start = time() + for i in range(100): + print("\tTaking 100 0.1-second videos to ./camera_dump/tiny-%02f" % i, duration = 0.1, show_preview = False) + picam2.close() + end = time() + print("\t\t^done. Took " + str(end - start) + " seconds") + sleep_ms(5000) + # Take many short videos without .close() + print("\t Taking ten 1-second videos without .close() to ./camera_dump/open-?.jpg for ? = 0 .. 9") + start = time() + for i in range(10): + picam2.start_and_record_video("./camera_dump/open-" + str(i) + ".jpg", duration = 1, show_preview = False) + picam2.close() + end = time() + print("\t\t^done. Took " + str(end - start) + " seconds") + sleep_ms(5000) + print(" END test_vids()") + + + +## LAUNCH + +if __name__ == "__main__": + main() diff --git a/client/proof_of_concept/camera_dump/so-git-commits-this-folder.txt b/client/proof_of_concept/camera_dump/so-git-commits-this-folder.txt new file mode 100644 index 0000000..7aba642 --- /dev/null +++ b/client/proof_of_concept/camera_dump/so-git-commits-this-folder.txt @@ -0,0 +1 @@ +shitpost \ No newline at end of file diff --git a/client/proof_of_concept/cushion.py b/client/proof_of_concept/cushion.py new file mode 100644 index 0000000..628ebf0 --- /dev/null +++ b/client/proof_of_concept/cushion.py @@ -0,0 +1,5 @@ +""" +I have no idea how this is gonna work. We'll need to do our own PWM or something ig. +Will only do this after I have the hardware to play with properly. +~Gabe +""" diff --git a/client/proof_of_concept/display.py b/client/proof_of_concept/display.py new file mode 100644 index 0000000..bbf1c22 --- /dev/null +++ b/client/proof_of_concept/display.py @@ -0,0 +1,264 @@ +""" +File: + sitting-desktop-garden/client/proof_of_concept/display.py + +Purpose: + Hardware test for the PiicoDev display. + +Author: + Gabriel Field (47484306) + +Source: + Largely based on https://core-electronics.com.au/guides/raspberry-pi/piicodev-oled-ssd1306-raspberry-pi-guide/ + +NOTE: + Ensure that I2C is enabled on the RPi first! +""" + +## SECTION: Imports + +from PiicoDev_SSD1306 import * +from PiicoDev_Unified import sleep_ms # cross-platform compatible sleep function +from math import sin, cos, pi +import random + + + +## SECTION: How the .graph2D constructor works + +""" +PiicoDev_SSD1306 + .graph2D + .__init__( + self, + originX = 0, originY = HEIGHT-1, # SSD coordinates of BOTTOM-LEFT CORNER of the graph. Units: pixels + width = WIDTH, height = HEIGHT, # Width and height that the graph TAKES UP ON THE SSD. Units: pixels. + minValue = 0, maxValue = 255, # Minimum and maximum vertical value that can be dispalyed on the graph. Unitless. + # Note: horizontal values for graph are *literally* horizontal pixels. + c = 1, # Colour in which to draw graph. + bars = False # Use True for a bar chart. We'll probably never use this. + ) +""" + + + +## SECTION: main() + +def main(): + display = create_PiicoDev_SSD1306() + + print("Screen width is " + str(WIDTH)) + print("Screen height is " + str(HEIGHT)) + sleep_ms(2000) + + print("==== TEST: Basic functionality ====") + draw_lines(display) + draw_rectangles(display) + + print("==== TEST: Displaying text and images ====") + draw_text(display) + draw_images(display) + + print("==== TEST: Graph ====") + draw_graph(display) + + print("==== TEST: COMPLETE! ====") + + + +## TEST: Basic functionality + +def draw_lines(display): + print(" BEGIN draw_lines()") + sleep_ms(2000) + # Draw a line from (0, 0) to (WIDTH, HEIGHT) + print("\tDrawing line from (0, 0) to (WIDTH, HEIGHT)") + start_x = 0 + start_y = 0 + end_x = WIDTH + end_y = HEIGHT + colour = 1 # COLOURS: `0` for black or `1` for white + display.line(start_x, start_y, end_x, end_y, colour) # Logically draw this line + display.show() # Render all things logically drawn + sleep_ms(5000) + # Blank the screen + print("\tBlanking the screen") + display.fill(0) # Parameter is a COLOUR, either `0` for black or `1` for white. + display.show() # Render + sleep_ms(1000) + # Draw a funny thing + print("\tDrawing a funny shape") + points = [None] * 5 + for i in range(5): + points[i] = ( WIDTH / 2 + cos(2 * pi / 5 * i) * HEIGHT / 2 , # how tf do you continue a line again in python + HEIGHT / 2 + sin(2 * pi / 5 * i) * HEIGHT / 2 ) + for i in range(5): + display.line(points[i][0], points[i][1], points[(i + 2) % 5][0], points[(i + 2) % 5][1], 1) + display.line(points[i][0], points[i][1], points[(i + 3) % 5][0], points[(i + 3) % 5][1], 1) + display.show() # Render + sleep_ms(5000) + # Blank the screen + print("\tBlanking the screen") + display.fill(0) + display.show() + print(" END draw_lines()") + +def draw_rectangles(display): + print(" BEGIN draw_rectangles()") + sleep_ms(2000) + # Draw an unfilled rectangle + print("\tDrawing an unfilled rectangle") + start_x = 0 + start_y = 0 + end_x = WIDTH / 2 + end_y = HEIGHT / 2 + colour = 1 # white + display.rect(start_x, start_y, end_x, end_y, colour) # UNFILLED rectangle + display.show() # render + sleep_ms(5000) + # Blank screen + print("\tBlanking the screen") + display.fill(0) + display.show() + # Draw a filled rectangle + print("\tDrawing a filled rectangle") + display.fill_rect(start_x, start_y, end_x, end_y, colour) # FILLED rectangle + display.show() # render + sleep_ms(5000) + # Blank the screen + print("\tBlanking the screen") + display.fill(0) + display.show() + print(" END draw_rectangles()") + + + +## TEST: Text and images + +def draw_text(display): + print(" BEGIN draw_text()") + sleep_ms(2000) + + # This should link to the font-pet-me-128.dat file in the current directory. + # **I have no idea how to change the location where we store this file.** + + # Hello world + print("\tDrawing hello world") + text_to_display = "Hello, World!" + top_left_x = 0 + top_left_y = 0 + colour = 1 + display.text(text_to_display, top_left_x, top_left_y, colour) + display.show() # render + sleep_ms(5000) + # Long string + print("\tDrawing long string") + display.text("The quick brown fox jumped over the lazy dog", 0, 15, 1) + display.show() + sleep_ms(5000) + # Blank screen + print("\tBlanking screen") + display.fill(0) + display.show() + sleep_ms(5000) + # Goodbye world + print("\tDrawing goodbye world") + display.text("Goodbye, world T-T", 0, 0, 1) + display.show() + sleep_ms(5000) + # Blank the screen + print("\tBlanking the screen") + display.fill(0) + display.show() + print(" END draw_text") + +def draw_images(display): + print(" BEGIN draw_images()") + sleep_ms(2000) + # Images are drawn from .pbm (Portable BitMap image) files. + # They should have dimensions 128 x 64. + + # Draw PiicoDev logo + print("\tDisplaying PiicoDev logo") + colour = 1 + display.load_pbm("./resources/piicodev-logo.pbm", colour) + display.show() # render + sleep_ms(5000) + # Blank screen + print("\tBlanking screen") + display.fill(0) + display.show() + sleep_ms(5000) + # Draw hilarious image + print("\tDisplaying hilarious image") + display.load_pbm("./resources/hilarious.pbm", 1) + display.show() + sleep_ms(5000) + # Blank screen + print("\tBlanking screen") + display.fill(0) + display.show() + print(" END draw_images()") + + + +## TEST: Graph + +def draw_graph(display): + print(" BEGIN draw_graph()") + sleep_ms(2000) + + curve = display.graph2D(minValue = -1, maxValue = 1, height = HEIGHT, width = WIDTH) + # minValue= key is for the minimum value on the logical graph. This gets mapped into pixels automatically. + # maxValue= key is for the maximum value on the logical graph. This gets mapped into pixels automatically. + # height= key is for maximum display height. + # width= key is for maximum display width. + + # Display a sine curve + print("\tDisplaying a sine curve") + for x in range(WIDTH): + y_value = sin(2 * pi * x / WIDTH) + display.fill(0) # NOTE: I don't know if this is necessary or not. + display.updateGraph2D(curve, y_value) # Add to the graph + display.show() # render + sleep_ms(5000) + # Add to the same curve + print("\tAdding another period to that curve, slowly") + for x in range(WIDTH): + display.fill(0) # When you add to the curve, I reckon this will mean that the whole graph overwrites the previous one. Otherwise, you'd get white garbage all over the screen + display.updateGraph2D(curve, sin(2 * pi * x / WIDTH)) # idk what this will do + display.show() + sleep_ms(100) # wait 1/10 of a second so that we can see what this does + sleep_ms(5000) + # Use a new curve to plot random values + print("\tPlotting random values on a new curve, slowly") + curve = display.graph2D(minValue = 0, maxValue = 1) # omitted height= and width= keys default to HEIGHT and WIDTH + for _ in range(WIDTH * 2): + display.fill(0) + display.updateGraph2D(curve, random.random()) + display.show() + sleep_ms(100) + sleep_ms(5000) + # Plot two curves simultaneously + print("\tPlotting two curves simultaneously") + linear = display.graph2D(minValue = 0, maxValue = WIDTH) + sinusoidal = display.graph2D(minValue = -1, maxValue = 1) + for x in range(WIDTH): + display.fill(0) + display.updateGraph2D(linear, x) + display.updateGraph2D(sinusoidal, sin(2 * pi * x / WIDTH)) + display.show() + sleep_ms(5000) + # Blank display + print("\tBlanking display") + display.fill(0) + display.show() + print(" END draw_graph()") + + + +## LAUNCH + +if __name__ == "__main__": + main() + diff --git a/client/proof_of_concept/font-pet-me-128.dat b/client/proof_of_concept/font-pet-me-128.dat new file mode 100644 index 0000000000000000000000000000000000000000..ad4d9e3d0514af3fb3010ed1d35af26321291aec GIT binary patch literal 768 zcmX|9O>5gg5FJ}WUK3H5Vu}i)af4_}4nd?~oFMc;<+g$vH=sfTrYMnV4>5$c2D@bK z@z3a?ha7V1Z^$7(rHB3um-LMsI%4b(Wpk7xh;ddy|c5!2Snuy z$Np3j?ekzT`}z)%re)LsZdfLgn3SPQq;^Bs4R;kO6^2T|zxrLsv8qf~MIm!s_>OJW zDdeorA@(VA921>>JIaQyh<3^><1Ufov>!6xr)Qu0{lx)Ee0H%LUT$qIH4)#{gFh*EWWQ-l z{8ERx*{q>&oM#t@vR8^3LvB&svRH_#)Kdh-f=1ifCKne8)$ja^|`-+)JZ1pSN&#Ctr>SuU|q4S|s6FVmDfqQyey_-3{Gixu?sCMg`CjR9IazN0m86EF3JXTA2kz`;ESp0{30oM}QI zy&2{Cv_qyDaXRi01v-v(faewzMc@K8`qwU{lU~L1^ZN2q^rYkI=q-5h{$|w=pxax} Sqy1U+9_vQU!1(ok^Zf(-nxWPJ literal 0 HcmV?d00001 diff --git a/client/proof_of_concept/gpio.py b/client/proof_of_concept/gpio.py new file mode 100644 index 0000000..99fccf6 --- /dev/null +++ b/client/proof_of_concept/gpio.py @@ -0,0 +1,56 @@ +""" +File: + sitting-desktop-garden/client/proof_of_concept/gpio.py + +Purpose: + Hardware test for the GPIO pins, interfacing through RPi.GPIO. + +Author: + Gabriel Field (47484306) + +Source: + Largely based on https://elinux.org/RPi_GPIO_Code_Samples#RPi.GPIO + See also https://elinux.org/RPi_Low-level_peripherals#General_Purpose_Input.2FOutput_.28GPIO.29 +""" + +## SECTION: Imports + +import RPi.GPIO as GPIO # WARNING: This should be installed by default on the RPi, + # but not on your local machine. + # I haven't `poetry add`ed it (I tried, but it failed >:( ). +from PiicoDev_Unified import sleep_ms + + + +## SECTION: main() + +def main(): + print(" BEGIN main()...") + GPIO.setmode(GPIO.BOARD) # use P1 header pin numbering convention + + # Set data directions + GPIO.setup(17, GPIO.IN) + GPIO.setup(18, GPIO.OUT) + + print(" Will read from pin 17 into code; will write to pin 18.") + print(" Doing that in 5 seconds...") + sleep_ms(5000) + + input_value = GPIO.input(17) + print(" Read " + str(input_value) + " from pin 17") + GPIO.output(18, GPIO.HIGH) + print(" Wrote high to pin 18") + + print(" Writing low to pin 18 in 2 seconds") + sleep_ms(2000) + GPIO.output(18, GPIO.LOW) + + print(" END main()") + + + +## LAUNCH + +if __name__ == "__main__": + main() + diff --git a/client/proof_of_concept/resources/hilarious.pbm b/client/proof_of_concept/resources/hilarious.pbm new file mode 100644 index 0000000000000000000000000000000000000000..01006c996ee90f35d07d4083f92dd208e78152df GIT binary patch literal 1034 zcmZ|Ozl+pB6bJC{WOA8fFEA-1EMzL#GRh$-7H|Czwh(O>E6X&(70YFog|m{Y?5`9A zZ?UqRy~S26t`v=IAy|!Fie<;i4>w8S3xs^$ym^_Kkm>2*!NWv6IvwCYaoowr5b((% zp<<#C(1WFLW(Wx!Rj2HDj*x;}*OsxnLQHWoYsLi)5IgWi?OQzvaq%9bN$tI01zvoY z(`juLyFQ-W$Lipr59MLs)GKl9LAqqQs_Q55$RX0D_}VxaQ3BtSqOsjhX~eb5JAvJ> zD4&=qV-LS^R@tZ!RXs80>dK~cQ%{Xy<&>SBTFz%?@7SDefYcJ)7_VyCIBM!m>#CoT zb^SQbUi0EiXa9_dF~wD+9;bYr59i>FNqZ@y&+^iYF;LL8(V5PZIjFS*y8Wq(bDhjN zWgfd^i>=anoF?;V$vpmzueUPI$LDG7U1hUND!bhJmrSkm!}vy~8pFn4_+le{YWQhLwNK^wuJC;*&!w&-t-*cZPzNshpuIS zPd&%D>zLSIoV^{KP54ioA@6<%Sol3GVROxP5v^n)%|f~#oEx4h07*I~{TYWPcV!)mv` z6kHSE@jIO?g;s>A$oZF+#=p*Fl+R=CpQ27ODt$K}Ow7C)6~3FFl2kJd;=sO7G09wh kl{(JnS(?2#CMGajMZN_W`4_B;{ygko|F6*Z-+vSM2A>|mL;wH) literal 0 HcmV?d00001 diff --git a/client/proof_of_concept/servo_driver.py b/client/proof_of_concept/servo_driver.py new file mode 100644 index 0000000..2d93049 --- /dev/null +++ b/client/proof_of_concept/servo_driver.py @@ -0,0 +1,101 @@ +""" +File: + sitting-desktop-garden/client/proof_of_concept/servo_driver.py + +Purpose: + Hardware test for the PiicoDev servos and server driver. + +Author: + Gabriel Field (47484306) + +Source: + Largely based on https://core-electronics.com.au/guides/piicodev/piicodev-servo-driver-pca9685-getting-started-guide/#JEFNE3E +""" + +## SECTION: Imports + +from PiicoDev_Servo import PiicoDev_Servo, PiicoDev_Servo_Driver +from PiicoDev_Unified import sleep_ms +from math import sin, pi + + + +## SECTION: main() + +def main(): + print("==== TEST: Basic use ====") + controller = PiicoDev_Servo_Driver() # Initialise the Servo Driver Module + servo = PiicoDev_Servo(controller, 1) # Simple setup: Attach a servo to channel 1 of the controller with default properties + test_step(servo) + continuous_servo = PiicoDev_Servo(controller, 1, midpoint_us=1500, range_us=1800) # Connect a 360° servo to channel 2 + test_spinny(continuous_servo) + + print(" Hook up two servos, with channels 1 and 2") + print(" You have 10 seconds") + sleep_ms(10000) + print(" Assuming they're allg") + + print("==== TEST: Multiple servos ====") + + + print("==== TEST: COMPLETE! ====") + servo1 = PiicoDev_Servo(controller, 1) + servo2 = PiicoDev_Servo(controller, 2) + test_multiple(servo1, servo2) + + +## TEST: Basic use + +def test_step(servo): + print(" BEGIN test_step()") + sleep_ms(2000) + print("\tStepping the servo") + servo.angle = 0 + sleep_ms(1000) + servo.angle = 90 + print(" END test_step()") + +def test_spinny(continuous_servo): + print(" BEGIN test_spinny()") + sleep_ms(2000) + print("\tStopping") + continuous_servo.speed = 0 + sleep_ms(5000) + print("\tGoing full-speed forwards") + continuous_servo.speed = 1 + sleep_ms(5000) + print("\tGoing half-speed forwards") + continuous_servo.speed = 0.5 + sleep_ms(5000) + print("\tGoing half-speed backwards") + continuous_servo.speed = -0.5 + sleep_ms(5000) + print("\tGoing full-speed backwards") + continuous_servo.speed = -1 + sleep_ms(5000) + print("\tStopping") + continuous_servo.speed = 0 + sleep_ms(5000) + print(" END test_spinny()") + + + +## TEST: Multiple servos + +def test_multiple(servo1, servo2): + print(" BEGIN test_multiple()") + sleep_ms(2000) + # This takes 18 seconds + for i in range(1800): + servo1.angle = 90 + 90 * sin(i * 2 * pi / 1800) + servo2.angle = 90 - 90 * sin(i * 2 * pi / 1800) + sleep_ms(10) + print(" END test_multiple()") + + + + +## LAUNCH + +if __name__ == "__main__": + main()