Skip to content

Commit

Permalink
reworked app structure, separated app from lib
Browse files Browse the repository at this point in the history
  • Loading branch information
mgineer85 committed Oct 25, 2024
1 parent 9b7ad85 commit ae9f39d
Show file tree
Hide file tree
Showing 15 changed files with 727 additions and 422 deletions.
30 changes: 4 additions & 26 deletions node/__main__.py → node/app_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

import logging
import os
import signal
import threading
from contextlib import asynccontextmanager
from types import FrameType

from fastapi import FastAPI
from fastapi.exception_handlers import http_exception_handler, request_validation_exception_handler
Expand All @@ -29,31 +26,13 @@ def _create_basic_folders():

@asynccontextmanager
async def lifespan(_: FastAPI):
# workaround to free resources on shutdown and prevent stalling
# https://github.com/encode/uvicorn/issues/1579#issuecomment-1419635974

# start app
default_sigint_handler = signal.getsignal(signal.SIGINT)

def terminate_now(signum: int, frame: FrameType = None):
logger.info("shutting down app via signal handler")
container.stop()
default_sigint_handler(signum, frame)

if threading.current_thread() is not threading.main_thread():
# https://github.com/encode/uvicorn/pull/871
# Signals can only be listened to from the main thread.
# usually only during testing, but no need in testing for this.
logger.info("lifecycle hook not installing signal, because current_thread not main_thread")
else:
logger.info("lifecycle hook installing signal to handle app shutdown")
signal.signal(signal.SIGINT, terminate_now)

# deliver app
container.start()
logger.info("starting app")
yield
# Clean up
# container.stop()
logger.info("clean up")
container.stop()


def _create_app() -> FastAPI:
Expand All @@ -64,11 +43,10 @@ def _create_app() -> FastAPI:
raise RuntimeError(f"cannot create data folders, error: {exc}") from exc

_app = FastAPI(
title="Photobooth-App API",
title="Wigglecam Node API",
description="API may change any time.",
version=__version__,
contact={"name": "mgineer85", "url": "https://github.com/photobooth-app/photobooth-app", "email": "[email protected]"},
license_info={"name": "MIT", "url": "https://github.com/photobooth-app/photobooth-app/blob/main/LICENSE.md"},
docs_url="/api/doc",
redoc_url=None,
openapi_url="/api/openapi.json",
Expand Down
124 changes: 124 additions & 0 deletions node/app_minimal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import logging
from datetime import datetime
from threading import Condition

from gpiozero import Button as ZeroButton
from pydantic import BaseModel, Field, PrivateAttr
from pydantic_settings import BaseSettings, SettingsConfigDict

from .container import container
from .services.baseservice import BaseService

logger = logging.getLogger(__name__)


class ConfigGpioPeripherial(BaseModel):
shutterbutton_in_pin_name: str = Field(default="GPIO4")


class AppMinimalConfig(BaseSettings):
"""
AppConfig class glueing all together
In the case where a value is specified for the same Settings field in multiple ways, the selected value is determined as follows
(in descending order of priority):
1 Arguments passed to the Settings class initialiser.
2 Environment variables, e.g. my_prefix_special_function as described above.
3 Variables loaded from a dotenv (.env) file.
4 Variables loaded from the secrets directory.
5 The default field values for the Settings model.
"""

_processed_at: datetime = PrivateAttr(default_factory=datetime.now) # private attributes

# groups -> setting items
peripherial_gpio: ConfigGpioPeripherial = ConfigGpioPeripherial()

model_config = SettingsConfigDict(
env_file_encoding="utf-8",
# first in following list is least important; last .env file overwrites the other.
env_file=[".env.dev", ".env.test", ".env.primary", ".env.node"],
env_nested_delimiter="__",
case_sensitive=True,
extra="ignore",
)


class Button(ZeroButton):
def _fire_held(self):
# workaround for bug in gpiozero https://github.com/gpiozero/gpiozero/issues/697
# https://github.com/gpiozero/gpiozero/issues/697#issuecomment-1480117579
# Sometimes the kernel omits edges, so if the last
# deactivating edge is omitted held keeps firing. So
# check the current value and send a fake edge to
# EventsMixin to stop the held events.
if self.value:
super()._fire_held()
else:
self._fire_events(self.pin_factory.ticks(), False)


class GpioPeripherialService(BaseService):
def __init__(self, config: ConfigGpioPeripherial):
super().__init__()

# init arguments
self._config: ConfigGpioPeripherial = config

# define private props
self._shutterbutton_in: Button = None
self._shutterbutton_in_condition: Condition = None

# init private props
self._shutterbutton_in_condition: Condition = Condition()

def start(self):
super().start()

# shutter button in
self._shutterbutton_in = Button(pin=self._config.shutterbutton_in_pin_name, bounce_time=0.04)
self._shutterbutton_in.when_pressed = self._on_shutterbutton
logger.info(f"external trigger button on {self._shutterbutton_in}")

logger.debug(f"{self.__module__} started")

def stop(self):
super().stop()

if self._shutterbutton_in:
self._shutterbutton_in.close()

def _on_shutterbutton(self):
logger.info("shutter button pressed")
with self._shutterbutton_in_condition:
self._shutterbutton_in_condition.notify_all()

def wait_for_shutterbutton(self, timeout: float = None) -> None:
with self._shutterbutton_in_condition:
self._shutterbutton_in_condition.wait(timeout=timeout)


def main():
container.start()
logger.info("starting app")
appminimalconfig = AppMinimalConfig()
gpioservice = GpioPeripherialService(appminimalconfig.peripherial_gpio)
gpioservice.start()

try:
while True:
gpioservice.wait_for_shutterbutton()
container.synced_acquisition_service.execute_job()

except KeyboardInterrupt:
print("got Ctrl+C, exiting")

# Clean up
gpioservice.stop()
logger.info("clean up")
container.stop()


if __name__ == "__main__":
main()
10 changes: 5 additions & 5 deletions node/container.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import logging

from .config import appconfig
from .services.baseservice import BaseService
from .services.gpio_primary_clockwork import GpioPrimaryClockworkService
from .services.sync_aquisition_service import SyncedAcquisitionService
from .services.config import appconfig
from .services.loggingservice import LoggingService
from .services.sync_acquisition_service import SyncedAcquisitionService

logger = logging.getLogger(__name__)


# and as globals module:
class Container:
# container
gpio_primary_service = GpioPrimaryClockworkService(config=appconfig.primary_gpio)
synced_acquisition_service = SyncedAcquisitionService()
logging_service = LoggingService(config=appconfig.logging)
synced_acquisition_service = SyncedAcquisitionService(config=appconfig.syncedacquisition)

def _service_list(self) -> list[BaseService]:
# list used to start/stop services. List sorted in the order of definition.
Expand Down
6 changes: 3 additions & 3 deletions node/routers/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

from fastapi import APIRouter

from . import aquisition, system
from . import acquisition, system

__all__ = [
"aquisition", # refers to the 'aquisition.py' file
"acquisition", # refers to the 'acquisition.py' file
"system",
]

router = APIRouter(prefix="/api")
router.include_router(aquisition.router)
router.include_router(acquisition.router)
router.include_router(system.router)
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/aquisition",
tags=["aquisition"],
prefix="/acquisition",
tags=["acquisition"],
)


Expand All @@ -33,12 +33,12 @@ def video_stream():

@router.get("/setup")
def setup_job(job_id, number_captures):
pass
container.synced_acquisition_service.setup_job()


@router.get("/trigger")
def trigger_job(job_id):
pass
container.synced_acquisition_service.execute_job()


@router.get("/results")
Expand Down
Loading

0 comments on commit ae9f39d

Please sign in to comment.