From 4a0a7d7f9160e3c9598eb3fb5661e1993e3116c0 Mon Sep 17 00:00:00 2001 From: Mitchell Clark Date: Mon, 7 Oct 2024 14:03:10 +1100 Subject: [PATCH 1/9] ref: deleted debugging comment in PostureTracker because models are working on RPI now --- client/models/pose_detection/routines.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/models/pose_detection/routines.py b/client/models/pose_detection/routines.py index 447a190..a917dd9 100644 --- a/client/models/pose_detection/routines.py +++ b/client/models/pose_detection/routines.py @@ -146,12 +146,6 @@ def _save_period(self) -> None: if time.time() - self._start_time <= PERIOD_SECONDS: return - # DEBUG:: - print(" ======== DATABASE POWERED BY AI ========") - # FIXME: I'm not convinced that this is writing to the database on the rpi. - # It's probably a camera alignment issue. But I'm not gonna test that on the - # `posture_graphing_impossible_to_test` branch lol - # ::DEBUG period_end = datetime.now() posture = Posture( id_=None, From 61587ece564691c175f0d23ed3bce09f54a564a4 Mon Sep 17 00:00:00 2001 From: Limao Chang <80520563+LimaoC@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:59:10 +1000 Subject: [PATCH 2/9] docs: add __init__.py docstring for drivers module --- client/drivers/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/drivers/__init__.py b/client/drivers/__init__.py index e69de29..cf9e214 100644 --- a/client/drivers/__init__.py +++ b/client/drivers/__init__.py @@ -0,0 +1,3 @@ +""" +Code modules that interface directly with the Raspberry Pi and hardware devices. +""" From db3dcc2627d69acd434771faeb2f861d89dd3e87 Mon Sep 17 00:00:00 2001 From: Limao Chang <80520563+LimaoC@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:03:16 +1000 Subject: [PATCH 3/9] style: sort imports --- client/data/routines.py | 3 +-- client/drivers/data_structures.py | 17 +++++------------ client/drivers/login_system.py | 14 +++++--------- client/drivers/main.py | 21 +++++++-------------- 4 files changed, 18 insertions(+), 37 deletions(-) diff --git a/client/data/routines.py b/client/data/routines.py index 03abaa2..61fa5de 100644 --- a/client/data/routines.py +++ b/client/data/routines.py @@ -5,10 +5,9 @@ """ import sqlite3 - from datetime import datetime -from typing import Any, NamedTuple, Optional, Iterator from importlib import resources +from typing import Any, Iterator, NamedTuple, Optional import numpy as np from pydbml import PyDBML diff --git a/client/drivers/data_structures.py b/client/drivers/data_structures.py index 7de3092..f422906 100644 --- a/client/drivers/data_structures.py +++ b/client/drivers/data_structures.py @@ -9,21 +9,14 @@ ## SECTION: Imports import time -from typing import List -from PiicoDev_Switch import PiicoDev_Switch -from PiicoDev_SSD1306 import * -from PiicoDev_Servo import PiicoDev_Servo, PiicoDev_Servo_Driver - from datetime import datetime +from math import pi, sin from queue import Queue +from typing import List -# DEBUG: -from math import sin, pi - -# :DEBUG - - -## SECTION: Constants +from PiicoDev_Servo import PiicoDev_Servo, PiicoDev_Servo_Driver +from PiicoDev_SSD1306 import * +from PiicoDev_Switch import PiicoDev_Switch """ Sentinel value for an invalid user. """ EMPTY_USER_ID = -1 diff --git a/client/drivers/login_system.py b/client/drivers/login_system.py index d5adbb0..1459681 100644 --- a/client/drivers/login_system.py +++ b/client/drivers/login_system.py @@ -6,16 +6,12 @@ from typing import Callable import numpy as np - -from data.routines import next_user_id, create_user +from data.routines import create_user, next_user_id +from drivers.data_structures import (DOUBLE_RIGHT_BUTTON, LEFT_BUTTON, + RIGHT_BUTTON, HardwareComponents) +from models.face_recognition.recognition import (Status, get_face_match, + register_faces) from models.pose_detection.frame_capturer import RaspCapturer -from models.face_recognition.recognition import register_faces, get_face_match, Status -from drivers.data_structures import ( - HardwareComponents, - LEFT_BUTTON, - RIGHT_BUTTON, - DOUBLE_RIGHT_BUTTON, -) NUM_FACES = 5 QUIT = -4 diff --git a/client/drivers/main.py b/client/drivers/main.py index d2da545..f0f3480 100644 --- a/client/drivers/main.py +++ b/client/drivers/main.py @@ -10,27 +10,20 @@ ## SECTION: Imports import argparse import logging -from datetime import datetime, timedelta import time +from datetime import datetime, timedelta import RPi.GPIO as GPIO +from data.routines import (Posture, destroy_database, get_user_postures, + init_database, reset_registered_face_embeddings) +from drivers.data_structures import ControlledData, HardwareComponents +from drivers.login_system import RESET, handle_authentication +from models.pose_detection.frame_capturer import RaspCapturer from PiicoDev_SSD1306 import * from PiicoDev_Switch import * from PiicoDev_Unified import sleep_ms -from models.pose_detection.frame_capturer import RaspCapturer -from data.routines import ( - init_database, - destroy_database, - reset_registered_face_embeddings, - get_user_postures, - Posture, -) -from drivers.data_structures import ControlledData, HardwareComponents -from drivers.login_system import handle_authentication, RESET - -## SECTION: Global constants - +#: Pin to which the vibration motor is attached. This is D8 on the PiicoDev header. CUSHION_GPIO_PIN = 8 """ Pin to which the vibration motor is attached. This is D8 on the PiicoDev header. """ From 5a2dba34825c5f7020bcdf709e89df0ae5a02647 Mon Sep 17 00:00:00 2001 From: Limao Chang <80520563+LimaoC@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:04:51 +1000 Subject: [PATCH 4/9] style: make docstrings consistent, move member field docstrings to correct place, remove some debug messages --- client/data/routines.py | 6 +- client/drivers/camera_overlord.py | 3 +- client/drivers/data_structures.py | 234 ++++++++++-------------------- client/drivers/main.py | 189 +++++++++++------------- 4 files changed, 169 insertions(+), 263 deletions(-) diff --git a/client/data/routines.py b/client/data/routines.py index 61fa5de..7d33ff2 100644 --- a/client/data/routines.py +++ b/client/data/routines.py @@ -1,7 +1,5 @@ -"""Module for interacting with SQLite database - -Last tested: - 15-09 all functions tested by Gabe +""" +Module for interacting with SQLite database """ import sqlite3 diff --git a/client/drivers/camera_overlord.py b/client/drivers/camera_overlord.py index ab85f28..f55ea51 100644 --- a/client/drivers/camera_overlord.py +++ b/client/drivers/camera_overlord.py @@ -41,13 +41,12 @@ def handle_quit(signo, frame): f.close() except FileExistsError: print("Snapshot already exists") - + try: f = open("/tmp/big_brother.jpg", "x") f.close() except FileExistsError: print("Big Brother already exists") - while True: for _ in range(2 * 3): diff --git a/client/drivers/data_structures.py b/client/drivers/data_structures.py index f422906..3f76179 100644 --- a/client/drivers/data_structures.py +++ b/client/drivers/data_structures.py @@ -1,13 +1,10 @@ """ -Brief: - Data structures for use on the RPi Client. -File: - sitting-desktop-garden/client/drivers/data_structures.py +Data structures for use on the RPi Client. + Author: Gabriel Field (47484306), Mitchell Clark """ -## SECTION: Imports import time from datetime import datetime from math import pi, sin @@ -18,7 +15,7 @@ from PiicoDev_SSD1306 import * from PiicoDev_Switch import PiicoDev_Switch -""" Sentinel value for an invalid user. """ +#: Sentinel value for an invalid user. EMPTY_USER_ID = -1 LEFT_BUTTON = 0 @@ -26,50 +23,30 @@ DOUBLE_RIGHT_BUTTON = 2 -## SECTION: ControlledData - - class ControlledData: """ Data for passing around in client/drivers/main.do_everything(). There should only ever be one object of this class at a time. - Member fields: - self._failed : bool - True if this data is incomplete. - self._user_id : int - ID of current user. - self._posture_data : Queue[float] - Data from the ML models which has not been seen yet, and is to be displayed - on the posture graph. - self._last_snapshot_time : datetime - Time of the last successful pull of posture data from the SQLite database - self._last_cushion_time : datetime - Time of the last successful cushion feedback event. - self._last_plant_time : datetime - Time of the last successful plant feedback event. - self._last_sniff_time : datetime TODO: Change this so it doesn't say "sniff". Change the getters and setters - Time of the last successful scent feedback event. - Class invariant: self._failed ==> (all other variables are default values) """ - _failed: bool """True if this data is incomplete.""" - _user_id: int + _failed: bool """ID of current user.""" - _posture_data: Queue[float] + _user_id: int """Data updated through ML models, used for feedback.""" - _last_snapshot_time: datetime + _posture_data: Queue[float] """Time of the last successful pull of posture data from the SQLite database""" - _last_cushion_time: datetime + _last_snapshot_time: datetime """Time of the last successful cushion feedback event.""" - _last_plant_time: datetime + _last_cushion_time: datetime """Time of the last successful plant feedback event.""" - _last_sniff_time: datetime + _last_plant_time: datetime """Time of the last successful scent feedback event.""" + _last_sniff_time: datetime # SECTION: Constructors @@ -95,7 +72,7 @@ def make_empty(cls, user_id: int) -> "ControlledData": Construct a non-failed object of this class, with a provided user ID and empty posture data. Returns: - (ControlledData): An object of this class that is not failed, with legal user ID and empty posture data. + An object of this class that is not failed, with legal user ID and empty posture data. """ return_me = ControlledData() return_me._failed = False @@ -111,10 +88,7 @@ def make_empty(cls, user_id: int) -> "ControlledData": @classmethod def make_failed(cls) -> "ControlledData": """ - Construct a failed object of this class. - - Returns: - (ControlledData): An object of this class that is failed. + Construct and return a failed object of this class. """ return_me = ControlledData() return_me._failed = True @@ -130,10 +104,7 @@ def make_failed(cls) -> "ControlledData": 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. + Returns next thing to put on the DEBUG graph. TODO: Remove this method """ @@ -145,82 +116,71 @@ def DEBUG_get_next_posture_graph_value(self) -> int: def is_failed(self) -> bool: """ - Returns: - (bool): True iff this ControlledData is failed. + Returns True iff this ControlledData is failed. """ return self._failed def get_user_id(self) -> int: """ - Returns: - (int): The user id of this ControlledData. + Returns the user id of this ControlledData. """ return self._user_id def get_posture_data(self) -> Queue[float]: """ - Returns: - (POSTURE_DATA): The posture data stored in this ControlledData. + Returns 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. + Returns 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. + time: 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. + Returns 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. + time: 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. + Returns 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. + time: 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. + Returns 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. + time: the last time that the user was provided olfactory feedback. """ self._last_sniff_time = time @@ -231,14 +191,11 @@ def accept_new_posture_data( Update the internal store of posture data for the OLED display. Args: - posture_data : List[float] - New posture data to accept and merge with the current state of this object. + 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 for datum in posture_data: self._posture_data.put_nowait(datum) @@ -248,93 +205,72 @@ def get_cushion_posture_data( self, ) -> "CUSHION_POSTURE_DATA": # TODO: Decide what this type looks like """ - Returns: - (CUSHION_POSTURE_DATA): Posture data necessary for cushion feedback. + Returns posture data necessary for cushion feedback. + TODO: Implement this. """ - # DEBUG: print(" WARNING: get_cushion_posture_data() not implemented!") - # :DEBUG return None def get_plant_posture_data( self, ) -> "PLANT_POSTURE_DATA": # TODO: Decide what this type looks like """ - Returns: - (PLANT_POSTURE_DATA) Posture data necessary for plant feedback. + Returns posture data necessary for plant feedback. + TODO: Implement this. """ - # DEBUG: print(" WARNING: get_plant_posture_data() not implemented!") - # :DEBUG return None def get_sniff_posture_data( self, ) -> "SNIFF_POSTURE_DATA": # TODO: Decide what this type looks like """ - Returns: - (SNIFF_POSTURE_DATA): Posture data necessary for scent feedback. + Returns posture data necessary for scent feedback. + TODO: Implement this. """ - # DEBUG: print(" WARNING: get_sniff_posture_data() not implemented!") - # :DEBUG return None -## SECTION: Hardware packaged together - - class HardwareComponents: """ Hardware components packaged together into a class. - - Member fields: - 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. """ - button0: PiicoDev_Switch """A button with address switches set to [0, 0, 0, 0]""" - button1: PiicoDev_Switch + button0: PiicoDev_Switch """A button with address switches set to [0, 0, 0, 1]""" - display: PiicoDev_SSD1306 + button1: PiicoDev_Switch """OLED SSD1306 Display with default address""" - posture_graph: PiicoDev_SSD1306.graph2D | None + display: PiicoDev_SSD1306 """ 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. """ - posture_graph_from: int | None + posture_graph: PiicoDev_SSD1306.graph2D | None """y-coordinate from which the posture graph begins, or `None` if no posture graph is active.""" - - plant_mover : PiicoDev_Servo + posture_graph_from: int | None + """ Continuous rotation servo driving the I. Jensen Plant Mover 10000. Its `midpoint_us` is `1600`. """ - plant_height : int + plant_mover: PiicoDev_Servo """Height of the plant, With a maximum given by (_PLANT_SHAFT_TURNS - _PLANT_SHAFT_SAFETY_BUFFER_TURNS - 1).""" - _PLANT_SHAFT_TURNS : int = 13 + plant_height: int """Maximum number of turns that can be made on the plant-moving shaft before damaging the product.""" - _PLANT_SHAFT_SAFETY_BUFFER_TURNS : int = 3 + _PLANT_SHAFT_TURNS: int = 13 """Number of turns on the plant-moving shaft to leave as a buffer, to ensure we don't damage the product.""" - _PLANT_GEAR_RATIO : float = 2 + _PLANT_SHAFT_SAFETY_BUFFER_TURNS: int = 3 """ Gear ratio between the plant-moving shaft and the continuous servo controlled by this HardwareComponents. To obtain `x` full rotations of the plant-moving shaft, make `x * _PLANT_GEAR_RATIO` full rotations of the `plant_mover`. """ - _PLANT_MOVER_PERIOD : float = 1000 * 60 / 55 + _PLANT_GEAR_RATIO: float = 2 """ Period (in milliseconds) for one full turn of the continuous rotation servo. To make a full turn of the continuous rotation servo, set its `.speed` to `_FULL_SPEED_UPWARDS` or @@ -343,20 +279,21 @@ class HardwareComponents: TODO: Check this value against what happens when we put the plant mover on it. NOTE: This is NON-LINEAR with the `.speed` attribute, for whatever reason. """ - _BASE_FULL_SPEED = 0.1 + _PLANT_MOVER_PERIOD: float = 1000 * 60 / 55 """Top speed for the `PiicoDev_Servo` in our application.""" - _FULL_SPEED_UPWARDS = _BASE_FULL_SPEED * (4 / 7) * (8 / 9) * 2 + _BASE_FULL_SPEED = 0.1 """ Value for the `PiicoDev_Servo`'s `.speed` attribute when moving the plant up. TODO: Check this value indeed drives the plant UP, not down. NOTE: This is asymmetric with `_FULL_SPEED_DOWNWARDS`. I don't know why. """ - _FULL_SPEED_DOWNWARDS = (-1) * _BASE_FULL_SPEED * (4 / 5) * 2 + _FULL_SPEED_UPWARDS = _BASE_FULL_SPEED * (4 / 7) * (8 / 9) * 2 """ Value for the `PiicoDev_Servo`'s `.speed` attribute when moving the plant down. TODO: Check this value indeed drives the plant UP, not down. NOTE: This is asymmetric with `_FULL_SPEED_DOWNWARDS`. I don't know why. """ + _FULL_SPEED_DOWNWARDS = (-1) * _BASE_FULL_SPEED * (4 / 5) * 2 # SECTION: Constructors @@ -375,7 +312,7 @@ def make_fresh(cls): 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. - PiicoDev_Servo(PiicoDev_Servo_Driver(), 1, midpoint_us=1600, range_us=1800) + PiicoDev_Servo(PiicoDev_Servo_Driver(), 1, midpoint_us=1600, range_us=1800), ) def __init__(self, button0, button1, display, plant_mover): @@ -384,26 +321,23 @@ def __init__(self, button0, button1, display, plant_mover): self.display: PiicoDev_SSD1306 = display self.posture_graph: PiicoDev_SSD1306.graph2D | None = None self.posture_graph_from: int | None = None - self.plant_mover : PiicoDev_Servo = plant_mover - self.plant_height : int = 0 + self.plant_mover: PiicoDev_Servo = plant_mover + self.plant_height: int = 0 self.plant_mover.speed = 0 # Stop the plant mover from spinning. self.unwind_plant() # SECTION: Setters - # 2024-09-02 14-45 Gabe: TESTED. def get_control_messages(self, user_id: int) -> List[str]: """ Get messages to display during usual application loop. TODO: Finalise these! Args: - user_id : int - ID of the currently logged-in user. + user_id: ID of the currently logged-in user. Returns: - (List[str]): The messages to display to the user during - the main application loop. + The messages to display to the user during the main application loop. """ return ["b0: logout", "id: " + str(user_id)] @@ -413,8 +347,7 @@ 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. + user_id: ID of the currently logged-in user. """ CONTROL_MESSAGES = self.get_control_messages(user_id) GRAPH_MIN_VALUE = 0 @@ -457,7 +390,7 @@ def unwind_plant(self) -> None: self.plant_mover.speed = self._FULL_SPEED_UPWARDS time.sleep(16 * self._PLANT_MOVER_PERIOD * self._PLANT_GEAR_RATIO / 1000) self.plant_mover.speed = 0 - + def wind_plant_safe(self) -> None: """ Wind the plant down to its minimum (safe) height. @@ -472,35 +405,41 @@ def wind_plant_safe(self) -> None: ) self.plant_mover.speed = 0 self.plant_height = 0 - + def set_plant_height(self, new_height: int) -> None: """ Move the plant to a desired height. Args: - new_height : int - Height to which to drive the I. Jensen Plant Mover 10000 + new_height: height to which to drive the I. Jensen Plant Mover 10000 """ self.plant_mover.speed = 0 - # DEBUG:: print(f" set_plant_height: {self.plant_height=}, {new_height=}") - # ::DEBUG distance = new_height - self.plant_height distance = distance if distance > 0 else (-1) * distance if new_height == self.plant_height: self.plant_mover.speed = 0 return - if new_height > self._PLANT_SHAFT_TURNS - self._PLANT_SHAFT_SAFETY_BUFFER_TURNS - 1: - print(" Plant mover not schmovin': can't get that high mate, that's just unsafe") + if ( + new_height + > self._PLANT_SHAFT_TURNS - self._PLANT_SHAFT_SAFETY_BUFFER_TURNS - 1 + ): + print( + " Plant mover not schmovin': can't get that high mate, that's just unsafe" + ) self.plant_mover.speed = 0 return if new_height < 0: - print(" Plant mover not schmovin': can't get that low mate, that's just dirty") + print( + " Plant mover not schmovin': can't get that low mate, that's just dirty" + ) self.plant_mover.speed = 0 return if new_height > self.plant_height: self.plant_mover.speed = self._FULL_SPEED_UPWARDS - time.sleep(distance * self._PLANT_MOVER_PERIOD * self._PLANT_GEAR_RATIO / 1000) + time.sleep( + distance * self._PLANT_MOVER_PERIOD * self._PLANT_GEAR_RATIO / 1000 + ) self.plant_mover.speed = 0 self.plant_height = new_height return @@ -510,7 +449,6 @@ def set_plant_height(self, new_height: int) -> None: self.plant_height = new_height return - # 2024-09-01 16:57 Gabe: TESTED. def oled_display_text(self, text: str, x: int, y: int, colour: int = 1) -> int: """ Display text on the oled display, wrapping lines if necessary. @@ -518,19 +456,13 @@ def oled_display_text(self, text: str, x: int, y: int, colour: int = 1) -> int: NOTE: Does not render. Call `.display.show()` if needed. 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 - Defaults to 1: white. + text: string to write to the OLED display. + x: horizontal coordinate from left side of screen. + y: vertical coordinate from top side of screen. + colour: 0 (black), 1 (white), defaults to 1. Returns: - (int): The y-value at which any subsequent lines should start printing from. + The y-value at which any subsequent lines should start printing from. """ LINE_HEIGHT = 15 # pixels LINE_WIDTH = 16 # characters @@ -539,7 +471,6 @@ def oled_display_text(self, text: str, x: int, y: int, colour: int = 1) -> int: 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: """ Display many lines of text on the oled display, wrapping lines if necessary. @@ -547,18 +478,13 @@ def oled_display_texts(self, texts: List[str], x: int, y: int, colour: int) -> i NOTE: Does not render. Call `.display.show()` if needed. Args: - 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 + texts: strings to write to the OLED display. Each string begins on a new line. + x: horizontal coordinate from left side of screen. + y: vertical coordinate from top side of screen. + colour: 0 (black), 1 (white) Returns: - (int): The y-value at which any subsequent lines should start printing from. + The y-value at which any subsequent lines should start printing from. """ display_height_offset = 0 for text in texts: @@ -600,8 +526,8 @@ def wait_for_button_press(self) -> int: if pressed_button != -1: self._clear_buttons() return pressed_button - - time.sleep(0.5) # DEBUG + + time.sleep(0.5) def _clear_buttons(self) -> None: """Clear pressed status from all buttons.""" diff --git a/client/drivers/main.py b/client/drivers/main.py index f0f3480..1bf935d 100644 --- a/client/drivers/main.py +++ b/client/drivers/main.py @@ -1,13 +1,10 @@ """ -Brief: - Entry point for the Sitting Desktop Garden Raspberry Pi Pot Client. -File: - sitting-desktop-garden/client/drivers/main.py +Entry point for the Sitting Desktop Garden Raspberry Pi Pot Client. + Author: Gabriel Field (47484306), Mitchell Clark """ -## SECTION: Imports import argparse import logging import time @@ -25,68 +22,65 @@ #: Pin to which the vibration motor is attached. This is D8 on the PiicoDev header. CUSHION_GPIO_PIN = 8 -""" Pin to which the vibration motor is attached. This is D8 on the PiicoDev header. """ +#: 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 each time the button is polled during wait_for_login_attempt(). """ +#: Number of milliseconds between pictures taken for login attempts. LOGIN_TAKE_PICTURE_INTERVAL = 1000 -""" Number of milliseconds between pictures taken for login attempts. """ +#: Number of milliseconds between starting to attempt_login() and taking the first picture. START_LOGIN_ATTEMPTS_DELAY = 3000 -""" Number of milliseconds between starting to attempt_login() and taking the first picture. """ +#: 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 each time the button is polled during ask_create_new_user(). """ +#: 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 completely failed and returning from attempt_login(). """ +#: Number of milliseconds between telling the user that login has succeeded and beginning real functionality. LOGIN_SUCCESS_DELAY = 3000 -""" Number of milliseconds between telling the user that login has succeeded and beginning real functionality. """ +#: Width (in pixels) of each individual entry on the posture graph. (One pixel is hard to read.) POSTURE_GRAPH_DATUM_WIDTH = 5 -""" Width (in pixels) of each individual entry on the posture graph. (One pixel is hard to read.) """ +#: Number of milliseconds between the user successfully logging out and returning to main(). LOGOUT_SUCCESS_DELAY = 3000 -""" Number of milliseconds between the user successfully logging out and returning to main(). """ +#: Minimum delay between reading posture data from the SQLite database, in do_everything(). GET_POSTURE_DATA_TIMEOUT = timedelta(milliseconds=10000) -""" Minimum delay between reading posture data from the SQLite database, in do_everything(). """ +#: Proportion of the time the user must be in frame for any feedback to be given. FIXME: Fine-tune this value later. PROPORTION_IN_FRAME_THRESHOLD = 0.3 -""" Proportion of the time the user must be in frame for any feedback to be given. FIXME: Fine-tune this value later. """ -HANDLE_CUSHION_FEEDBACK_TIMEOUT = timedelta(milliseconds=10000) # DEBUG -""" Minimum delay between consecutive uses of the vibration motor. Used in handle_feedback(). """ +#: Minimum delay between consecutive uses of the vibration motor. Used in handle_feedback(). +HANDLE_CUSHION_FEEDBACK_TIMEOUT = timedelta(milliseconds=10000) # DEBUG +#: Length of time for which the vibration motor should vibrate. Used in handle_cushion_feedback(). CUSHION_ACTIVE_INTERVAL = timedelta(milliseconds=1000) -""" Length of time for which the vibration motor should vibrate. Used in handle_cushion_feedback(). """ +#: Threshold for vibration cushion feedback. If the proportion of "good" sitting posture is below this, the cushion will vibrate. +#: FIXME: Fine-tune this value later. CUSHION_PROPORTION_GOOD_THRESHOLD = 0.5 -""" -Threshold for vibration cushion feedback. If the proportion of "good" sitting posture is below this, the cushion will vibrate. -FIXME: Fine-tune this value later. -""" -HANDLE_PLANT_FEEDBACK_TIMEOUT = timedelta(milliseconds=2000) # DEBUG: used to be 10000 -""" Minimum delay between consecutive uses of the plant-controlling servos. Used in handle_feedback(). """ +#: Minimum delay between consecutive uses of the plant-controlling servos. Used in handle_feedback(). +HANDLE_PLANT_FEEDBACK_TIMEOUT = timedelta(milliseconds=2000) # DEBUG: used to be 10000 +#: Threshold for I. Jensen Plant Mover 10000 feedback. If the proportion of "good" sitting posture is below this, +#: the plant will move down. +#: FIXME: Fine-tune this value later. PLANT_PROPORTION_GOOD_THRESHOLD = 0.5 -""" -Threshold for I. Jensen Plant Mover 10000 feedback. If the proportion of "good" sitting posture is below this, -the plant will move down. -FIXME: Fine-tune this value later. -""" # KILLME: +#: Minimum delay between consecutive uses of the scent bottle-controlling servos. Used in handle_feedback(). HANDLE_SNIFF_FEEDBACK_TIMEOUT = timedelta(milliseconds=20000) -""" Minimum delay between consecutive uses of the scent bottle-controlling servos. Used in handle_feedback(). """ +#: DEBUG Number of milliseconds between each loop iteration in do_everything(). DEBUG_DO_EVERYTHING_INTERVAL = 1000 -""" DEBUG Number of milliseconds between each loop iteration in do_everything(). """ logger = logging.getLogger(__name__) -## SECTION: main() - def main(): """ Entry point for the control program. """ parser = argparse.ArgumentParser() - parser.add_argument("--no-posture-model", action="store_true") + parser.add_argument( + "--no-posture-model", + action="store_true", + help="Whether to run the posture model. Useful for debugging.", + ) args = parser.parse_args() logging.basicConfig(level=logging.DEBUG) @@ -95,37 +89,45 @@ def main(): logger.debug("Initialising database") init_database() + # Spin up the posture tracking process if not args.no_posture_model: from models.pose_detection.routines import PostureProcess logger.debug("Initialising posture tracking process") posture_process = PostureProcess(frame_capturer=RaspCapturer) + # Handle user login/registration, posture tracking, and running the user session + # Under normal circumstances, this loop shouldn't exit while True: + # Attempt to login an existing user or register a new user user_id = handle_authentication(hardware) - # Handle reset + # Handle a "hard" reset, which resets the database, all registered faces, + # and the hardware if user_id == RESET: _reset_garden() continue # Create user session data user = ControlledData.make_empty(user_id) + + # Let the posture tracking process know about the current user's id' if not args.no_posture_model: posture_process.track_user(user_id) + logger.debug("Login successful") # Run user session do_everything(user) + # Let the posture tracking process if the user has logged out if not args.no_posture_model: posture_process.untrack_user() -## SECTION: Hardware initialisation +# SECTION: Hardware initialisation -# 2024-09-01_15-29 Gabe: TESTED. for buttons and OLED display. def initialise_hardware() -> HardwareComponents: """ Set up hardware for use throughout the project. We have: @@ -135,11 +137,10 @@ def initialise_hardware() -> HardwareComponents: Vibration motor attached to GPIO pin BUZZER_GPIO_PIN Returns: - (HardwareComponents): Object consisting of all hardware components connected to the Raspberry Pi. + 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). """ - print(" initialise_hardware()") # DEBUG + print(" initialise_hardware()") return_me = HardwareComponents.make_fresh() # Clear button queues return_me.button0.was_pressed @@ -150,26 +151,22 @@ def initialise_hardware() -> HardwareComponents: # Write low to stop buzzer from mistakenly buzzing, if necessary GPIO.output(CUSHION_GPIO_PIN, GPIO.LOW) - print(" initialise_hardware() FINISHED") # DEBUG + print(" initialise_hardware() FINISHED") return return_me -## SECTION: Control for the logged-in user +# SECTION: Control for the logged-in user -# 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: - (auspost : ControlledData): Data encapsulating the current state of the program. + auspost: data encapsulating the current state of the program. + Requires: ! auspost.is_failed() - - TODO: Actually implement this """ print(" BEGIN do_everything()") @@ -217,21 +214,23 @@ def do_everything(auspost: ControlledData) -> None: sleep_ms(DEBUG_DO_EVERYTHING_INTERVAL) -# 2024-09-13 11-32 Gabe: TESTED. def update_display_screen(auspost: ControlledData) -> bool: """ Update the display screen with whatever needs to be on there. + We will display: As per HardwareComponents.get_control_messages(), and Current-session posture graph Args: - (auspost : ControlledData): - Data encapsulating the current state of the program. + auspost: 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. + True, always. If you get a False return value, then something has gone VERY wrong. + Requires: ! auspost.is_failed() + Ensures: ! auspost.is_failed() """ @@ -255,9 +254,7 @@ def update_display_screen(auspost: ControlledData) -> bool: def handle_posture_monitoring_new(auspost: ControlledData) -> bool: - # DEBUG: print(" handle_posture_monitoring_new()") - # :DEBUG now = datetime.now() @@ -270,11 +267,9 @@ def handle_posture_monitoring_new(auspost: ControlledData) -> bool: period_end=now, ) - # DEBUG:: # Exit if not enough data # if len(recent_posture_data) <= POSTURE_GRAPH_DATUM_WIDTH: if len(recent_posture_data) == 0: - # ::DEBUG print(" Exiting handle_posture_monitoring_new() early: Not enough data") # auspost.set_last_snapshot_time(datetime.now()) return True @@ -297,8 +292,10 @@ def handle_posture_monitoring_new(auspost: ControlledData) -> bool: # Calculate total time span start_time = recent_posture_data[0].period_start - end_time = recent_posture_data[-1].period_end # DEBUG: Used to be period_start - total_time = end_time - start_time # BUG: This calculation sometimes results in `0`, which gets divided by later... + end_time = recent_posture_data[-1].period_end # DEBUG: Used to be period_start + total_time = ( + end_time - start_time + ) # BUG: This calculation sometimes results in `0`, which gets divided by later... # Calculate the interval length interval = total_time / POSTURE_GRAPH_DATUM_WIDTH @@ -336,34 +333,28 @@ def handle_posture_monitoring_new(auspost: ControlledData) -> bool: return True -# KILLME: +# SECTION: Feedback handling def handle_posture_monitoring(auspost: ControlledData) -> bool: """ Take a snapshot monitoring the user, and update the given ControlledData if necessary. Args: - (auspost : ControlledData): Data encapsulating the current state of the program. + auspost: 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 error handling - WARNING: UNTESTED! """ - # DEBUG: print(" handle_posture_monitoring()") - # :DEBUG 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([]) - # DEBUG: auspost.accept_new_posture_data([auspost.DEBUG_get_next_posture_graph_value()]) - # :DEBUG auspost.set_last_snapshot_time(now) return True @@ -373,11 +364,13 @@ def handle_feedback(auspost: ControlledData) -> bool: Provide feedback to the user if necessary. Args: - (auspost : ControlledData): Data encapsulating the current state of the program. + auspost: 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() """ @@ -397,28 +390,23 @@ def handle_feedback(auspost: ControlledData) -> bool: return True -## SECTION: Feedback handling - - -# 2024-09-15_20-18 Gabe: TESTED. 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. + auspost: 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. + 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 # Load posture records within the last HANDLE_CUSHION_FEEDBACK_TIMEOUT now = datetime.now() @@ -430,12 +418,10 @@ def handle_cushion_feedback(auspost: ControlledData) -> bool: ) # Conditions for exiting early - # 2024-09-15_19-47 Gabe: TESTED. if len(recent_posture_data) == 0: print(" Exiting handle_cushion_feedback() early: No data") auspost.set_last_cushion_time(datetime.now()) return True - # 2024-09-15_20-18 Gabe: TESTED. average_prop_in_frame = sum( [posture.prop_in_frame for posture in recent_posture_data] ) / len(recent_posture_data) @@ -445,7 +431,6 @@ def handle_cushion_feedback(auspost: ControlledData) -> bool: ) auspost.set_last_cushion_time(datetime.now()) return True - # 2024-09-15_20-18 Gabe: TESTED. average_prop_good = sum( [posture.prop_good for posture in recent_posture_data] ) / len(recent_posture_data) @@ -454,7 +439,6 @@ def handle_cushion_feedback(auspost: ControlledData) -> bool: auspost.set_last_cushion_time(datetime.now()) return True - # 2024-09-15_19-40 Gabe: TESTED. buzzer_start_time = datetime.now() GPIO.output(CUSHION_GPIO_PIN, GPIO.HIGH) print(" buzzer on") @@ -474,19 +458,17 @@ def handle_plant_feedback(auspost: ControlledData) -> bool: of when plant feedback was last given. Args: - (auspost : ControlledData): Data encapsulating the current state of the program. + auspost: 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 now = datetime.now() @@ -504,7 +486,7 @@ def handle_plant_feedback(auspost: ControlledData) -> bool: print(" Exiting handle_plant_feedback() early: No data") auspost.set_last_plant_time(datetime.now()) return True - + average_prop_in_frame = sum( [posture.prop_in_frame for posture in recent_posture_data] ) / len(recent_posture_data) @@ -514,38 +496,39 @@ def handle_plant_feedback(auspost: ControlledData) -> bool: ) auspost.set_last_plant_time(datetime.now()) return True - + # Calculate average proportion average_prop_good = sum( [posture.prop_good for posture in recent_posture_data] ) / len(recent_posture_data) - + # Judge. if average_prop_good >= PLANT_PROPORTION_GOOD_THRESHOLD: hardware.set_plant_height(hardware.plant_height + 1) else: hardware.set_plant_height(hardware.plant_height - 1) - + 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. + 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. + auspost: 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. + 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_sniff_feedback()") @@ -567,10 +550,10 @@ def _reset_garden() -> None: reset_registered_face_embeddings() print("\t initialising hardware...") hardware = initialise_hardware() - print("\t Like a phoenex, the Sitting Desktop Garden rises anew") + print("\t Like a phoenix, the Sitting Desktop Garden rises anew") -## LAUNCH +# LAUNCH if __name__ == "__main__": hardware = initialise_hardware() From d952274da6bafa277f368a6e227a7438d1225f27 Mon Sep 17 00:00:00 2001 From: Limao Chang <80520563+LimaoC@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:17:01 +1000 Subject: [PATCH 5/9] style: revert back to black-style imports --- client/drivers/login_system.py | 11 +++++++---- client/drivers/main.py | 9 +++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/client/drivers/login_system.py b/client/drivers/login_system.py index 1459681..fee192f 100644 --- a/client/drivers/login_system.py +++ b/client/drivers/login_system.py @@ -7,10 +7,13 @@ import numpy as np from data.routines import create_user, next_user_id -from drivers.data_structures import (DOUBLE_RIGHT_BUTTON, LEFT_BUTTON, - RIGHT_BUTTON, HardwareComponents) -from models.face_recognition.recognition import (Status, get_face_match, - register_faces) +from drivers.data_structures import ( + HardwareComponents, + LEFT_BUTTON, + RIGHT_BUTTON, + DOUBLE_RIGHT_BUTTON, +) +from models.face_recognition.recognition import Status, get_face_match, register_faces from models.pose_detection.frame_capturer import RaspCapturer NUM_FACES = 5 diff --git a/client/drivers/main.py b/client/drivers/main.py index 1bf935d..fc14433 100644 --- a/client/drivers/main.py +++ b/client/drivers/main.py @@ -11,8 +11,13 @@ from datetime import datetime, timedelta import RPi.GPIO as GPIO -from data.routines import (Posture, destroy_database, get_user_postures, - init_database, reset_registered_face_embeddings) +from data.routines import ( + init_database, + destroy_database, + reset_registered_face_embeddings, + get_user_postures, + Posture, +) from drivers.data_structures import ControlledData, HardwareComponents from drivers.login_system import RESET, handle_authentication from models.pose_detection.frame_capturer import RaspCapturer From 37628d6e18bf585eba95f841150e956c988e76a6 Mon Sep 17 00:00:00 2001 From: Limao Chang <80520563+LimaoC@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:52:57 +1000 Subject: [PATCH 6/9] style: move attribute descriptions to overall class docstring --- client/drivers/data_structures.py | 95 ++++++++++++++++--------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/client/drivers/data_structures.py b/client/drivers/data_structures.py index 3f76179..69628de 100644 --- a/client/drivers/data_structures.py +++ b/client/drivers/data_structures.py @@ -29,21 +29,23 @@ class ControlledData: There should only ever be one object of this class at a time. + Attributes: + _failed: True if this data is incomplete. + _user_id: ID of current user. + _posture_data: Data updated through ML models, used for feedback + _last_snapshot_time: Time of the last successful pull of posture data from the SQLite database + _last_cushion_time: Time of the last successful cushion feedback event. + _last_plant_time: Time of the last successful plant feedback event. + Class invariant: self._failed ==> (all other variables are default values) """ - """True if this data is incomplete.""" _failed: bool - """ID of current user.""" _user_id: int - """Data updated through ML models, used for feedback.""" _posture_data: Queue[float] - """Time of the last successful pull of posture data from the SQLite database""" _last_snapshot_time: datetime - """Time of the last successful cushion feedback event.""" _last_cushion_time: datetime - """Time of the last successful plant feedback event.""" _last_plant_time: datetime """Time of the last successful scent feedback event.""" _last_sniff_time: datetime @@ -238,61 +240,63 @@ def get_sniff_posture_data( class HardwareComponents: """ Hardware components packaged together into a class. + + Attributes: + button0: A button with address switches set to [0, 0, 0, 0] + button1: A button with address switches set to [0, 0, 0, 1] + display: OLED SSD1306 Display with default address + posture_graph: 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. + posture_graph_from: y-coordinate from which the posture graph begins, or `None` + if no posture graph is active. + + plant_mover: Continuous rotation servo driving the I. Jensen Plant Mover 10000. + Its `midpoint_us` is `1600`. + plant_height: Height of the plant, With a maximum given by + (_PLANT_SHAFT_TURNS - _PLANT_SHAFT_SAFETY_BUFFER_TURNS - 1). + _PLANT_SHAFT_TURNS: Maximum number of turns that can be made on the plant-moving + shaft before damaging the product. + _PLANT_SHAFT_SAFETY_BUFFER_TURNS: Number of turns on the plant-moving shaft to + leave as a buffer, to ensure we don't damage + the product. + _PLANT_GEAR_RATIO: Gear ratio between the plant-moving shaft and the continuous + servo controlled by this HardwareComponents. To obtain `x` + full rotations of the plant-moving shaft, make + `x * _PLANT_GEAR_RATIO` full rotations of the `plant_mover`. + _PLANT_MOVER_PERIOD: Period (in milliseconds) for one full turn of the + continuous rotation servo. To make a full turn of the + continuous rotation servo, set its `.speed` to + `_FULL_SPEED_UPWARDS` or `_FULL_SPEED_DOWNWARDS` and wait + `_PLANT_MOVER_PERIOD * _PLANT_GEAR_RATIO` milliseconds. + WARNING: This value may be different once we put some load on the plant mover! + TODO: Check this value against what happens when we put the plant mover on it. + NOTE: This is NON-LINEAR with the `.speed` attribute, for whatever reason. + _BASE_FULL_SPEED: Top speed for the `PiicoDev_Servo` in our application. + _FULL_SPEED_UPWARDS: Value for the `PiicoDev_Servo`'s `.speed` attribute when + moving the plant up. + TODO: Check this value indeed drives the plant UP, not down. + NOTE: This is asymmetric with `_FULL_SPEED_DOWNWARDS`. I don't know why. + _FULL_SPEED_DOWNWARDS: Value for the `PiicoDev_Servo`'s `.speed` attribute when + moving the plant down. + TODO: Check this value indeed drives the plant DOWN, not UP. """ - """A button with address switches set to [0, 0, 0, 0]""" button0: PiicoDev_Switch - """A button with address switches set to [0, 0, 0, 1]""" button1: PiicoDev_Switch - """OLED SSD1306 Display with default address""" display: PiicoDev_SSD1306 - """ - 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. - """ posture_graph: PiicoDev_SSD1306.graph2D | None - """y-coordinate from which the posture graph begins, or `None` if no posture graph is active.""" posture_graph_from: int | None - """ - Continuous rotation servo driving the I. Jensen Plant Mover 10000. - Its `midpoint_us` is `1600`. - """ plant_mover: PiicoDev_Servo - """Height of the plant, With a maximum given by (_PLANT_SHAFT_TURNS - _PLANT_SHAFT_SAFETY_BUFFER_TURNS - 1).""" plant_height: int - """Maximum number of turns that can be made on the plant-moving shaft before damaging the product.""" _PLANT_SHAFT_TURNS: int = 13 - """Number of turns on the plant-moving shaft to leave as a buffer, to ensure we don't damage the product.""" _PLANT_SHAFT_SAFETY_BUFFER_TURNS: int = 3 - """ - Gear ratio between the plant-moving shaft and the continuous servo controlled by this HardwareComponents. - To obtain `x` full rotations of the plant-moving shaft, make `x * _PLANT_GEAR_RATIO` full rotations of the - `plant_mover`. - """ _PLANT_GEAR_RATIO: float = 2 - """ - Period (in milliseconds) for one full turn of the continuous rotation servo. - To make a full turn of the continuous rotation servo, set its `.speed` to `_FULL_SPEED_UPWARDS` or - `_FULL_SPEED_DOWNWARDS` and wait `_PLANT_MOVER_PERIOD * _PLANT_GEAR_RATIO` milliseconds. - WARNING: This value may be different once we put some load on the plant mover! - TODO: Check this value against what happens when we put the plant mover on it. - NOTE: This is NON-LINEAR with the `.speed` attribute, for whatever reason. - """ _PLANT_MOVER_PERIOD: float = 1000 * 60 / 55 - """Top speed for the `PiicoDev_Servo` in our application.""" _BASE_FULL_SPEED = 0.1 - """ - Value for the `PiicoDev_Servo`'s `.speed` attribute when moving the plant up. - TODO: Check this value indeed drives the plant UP, not down. - NOTE: This is asymmetric with `_FULL_SPEED_DOWNWARDS`. I don't know why. - """ _FULL_SPEED_UPWARDS = _BASE_FULL_SPEED * (4 / 7) * (8 / 9) * 2 - """ - Value for the `PiicoDev_Servo`'s `.speed` attribute when moving the plant down. - TODO: Check this value indeed drives the plant UP, not down. - NOTE: This is asymmetric with `_FULL_SPEED_DOWNWARDS`. I don't know why. - """ _FULL_SPEED_DOWNWARDS = (-1) * _BASE_FULL_SPEED * (4 / 5) * 2 # SECTION: Constructors @@ -341,7 +345,6 @@ def get_control_messages(self, user_id: int) -> List[str]: """ return ["b0: logout", "id: " + str(user_id)] - # 2024-09-13 08-31 Gabe: TESTED. def initialise_posture_graph(self, user_id: int) -> None: """ Initialise self.posture_graph according to the provided user_id. From c40cad25abc5f2a61ffa4d07b31218409384946e Mon Sep 17 00:00:00 2001 From: Limao Chang <80520563+LimaoC@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:28:57 +1000 Subject: [PATCH 7/9] refactor: remove scent feedback code --- client/drivers/data_structures.py | 29 ----------------------------- client/drivers/main.py | 31 ------------------------------- 2 files changed, 60 deletions(-) diff --git a/client/drivers/data_structures.py b/client/drivers/data_structures.py index 69628de..ab0f116 100644 --- a/client/drivers/data_structures.py +++ b/client/drivers/data_structures.py @@ -47,8 +47,6 @@ class ControlledData: _last_snapshot_time: datetime _last_cushion_time: datetime _last_plant_time: datetime - """Time of the last successful scent feedback event.""" - _last_sniff_time: datetime # SECTION: Constructors @@ -62,7 +60,6 @@ def __init__(self): self._last_snapshot_time = datetime.now() 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) @@ -83,7 +80,6 @@ def make_empty(cls, user_id: int) -> "ControlledData": return_me._last_snapshot_time = datetime.now() 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 @@ -99,7 +95,6 @@ def make_failed(cls) -> "ControlledData": return_me._last_snapshot_time = datetime.now() return_me._last_cushion_time = datetime.now() return_me._last_plant_time = datetime.now() - return_me._last_sniff_time = datetime.now() return return_me # SECTION: Getters/Setters @@ -173,19 +168,6 @@ def set_last_plant_time(self, time: datetime) -> None: """ self._last_plant_time = time - def get_last_sniff_time(self) -> datetime: - """ - Returns 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: the last time that the user was provided olfactory feedback. - """ - self._last_sniff_time = time - def accept_new_posture_data( self, posture_data: List[float] ) -> None: # TODO: Refine type signature @@ -225,17 +207,6 @@ def get_plant_posture_data( print(" WARNING: get_plant_posture_data() not implemented!") return None - def get_sniff_posture_data( - self, - ) -> "SNIFF_POSTURE_DATA": # TODO: Decide what this type looks like - """ - Returns posture data necessary for scent feedback. - - TODO: Implement this. - """ - print(" WARNING: get_sniff_posture_data() not implemented!") - return None - class HardwareComponents: """ diff --git a/client/drivers/main.py b/client/drivers/main.py index fc14433..024497b 100644 --- a/client/drivers/main.py +++ b/client/drivers/main.py @@ -66,10 +66,6 @@ #: FIXME: Fine-tune this value later. PLANT_PROPORTION_GOOD_THRESHOLD = 0.5 -# KILLME: -#: Minimum delay between consecutive uses of the scent bottle-controlling servos. Used in handle_feedback(). -HANDLE_SNIFF_FEEDBACK_TIMEOUT = timedelta(milliseconds=20000) - #: DEBUG Number of milliseconds between each loop iteration in do_everything(). DEBUG_DO_EVERYTHING_INTERVAL = 1000 @@ -388,9 +384,6 @@ def handle_feedback(auspost: ControlledData) -> bool: 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 @@ -518,30 +511,6 @@ def handle_plant_feedback(auspost: ControlledData) -> bool: 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: Data encapsulating the current state of the program. - - Returns: - True, always. If you get a False return value, then something has gone VERY wrong. - - Requires: - ! auspost.is_failed() - - Ensures: - ! auspost.is_failed() - """ - # DEBUG: - print(" handle_sniff_feedback()") - # :DEBUG - auspost.set_last_sniff_time(datetime.now()) - return True - - def _reset_garden() -> None: """Reset data, faces and hardware.""" print(" Burning the garden to the ground...") From 6b05be72377ce98d01cdeeeb366a27f07481b935 Mon Sep 17 00:00:00 2001 From: Mitchell Clark Date: Fri, 11 Oct 2024 08:31:39 +1000 Subject: [PATCH 8/9] wip: started double register check --- client/models/face_recognition/recognition.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/models/face_recognition/recognition.py b/client/models/face_recognition/recognition.py index 043f8a3..f3f072a 100644 --- a/client/models/face_recognition/recognition.py +++ b/client/models/face_recognition/recognition.py @@ -72,6 +72,11 @@ def register_faces(user_id: int, faces: list[np.ndarray]) -> int: if not all(matches): return Status.TOO_MANY_FACES.value + # Ensure user is not already registered + for _, other_user_embeddings in iter_face_embeddings(): + for embedding in face_embeddings: + matches = face_recognition.compare_faces(other_user_embeddings, embedding) + register_face_embeddings(user_id, face_embeddings) return Status.OK.value From 743617821dca4c993e101358826e9f3cfec4cec7 Mon Sep 17 00:00:00 2001 From: MitchellJC <81349046+MitchellJC@users.noreply.github.com> Date: Fri, 11 Oct 2024 09:12:10 +1000 Subject: [PATCH 9/9] feat: added check for if user already registered --- client/drivers/login_system.py | 3 ++- client/models/face_recognition/recognition.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/client/drivers/login_system.py b/client/drivers/login_system.py index fee192f..b076ce7 100644 --- a/client/drivers/login_system.py +++ b/client/drivers/login_system.py @@ -17,12 +17,13 @@ from models.pose_detection.frame_capturer import RaspCapturer NUM_FACES = 5 -QUIT = -4 +QUIT = -6 RESET = -5 BAD_STATUS_MESSAGES = { Status.NO_FACES.value: "No face detected please", Status.TOO_MANY_FACES.value: "Too many faces detected", Status.NO_MATCH.value: "Could not match face", + Status.ALREADY_REGISTERED.value: "Face already registered", } QUIT_INSTRUCTIONS = "Right button to quit" diff --git a/client/models/face_recognition/recognition.py b/client/models/face_recognition/recognition.py index f3f072a..b322fbb 100644 --- a/client/models/face_recognition/recognition.py +++ b/client/models/face_recognition/recognition.py @@ -9,6 +9,7 @@ class Status(Enum): + ALREADY_REGISTERED = -4 NO_FACES = -3 TOO_MANY_FACES = -2 NO_MATCH = -1 @@ -77,6 +78,9 @@ def register_faces(user_id: int, faces: list[np.ndarray]) -> int: for embedding in face_embeddings: matches = face_recognition.compare_faces(other_user_embeddings, embedding) + if any(matches): + return Status.ALREADY_REGISTERED.value + register_face_embeddings(user_id, face_embeddings) return Status.OK.value