Skip to content

Commit

Permalink
moved dataclasses to dto modules
Browse files Browse the repository at this point in the history
  • Loading branch information
mgineer85 committed Nov 16, 2024
1 parent 0ed64b0 commit 4ba4de9
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 93 deletions.
12 changes: 2 additions & 10 deletions wigglecam/connector/cameranode.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
import logging
from dataclasses import asdict, dataclass
from dataclasses import asdict
from functools import cached_property

import requests

from wigglecam.services.jobservice import JobRequest

from .dto import NodeStatus
from .models import ConfigCameraNode

logger = logging.getLogger(__name__)


@dataclass
class NodeStatus:
description: str = None
can_connect: bool = None
is_healthy: bool = None
is_primary: bool = None
status: str = None


class CameraNode:
def __init__(self, config: ConfigCameraNode = None):
# init the arguments
Expand Down
9 changes: 4 additions & 5 deletions wigglecam/connector/camerapool.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

from wigglecam.services.jobservice import JobItem, JobRequest

from ..utils.simpledb import SimpleDb
from .cameranode import CameraNode, NodeStatus
from .models import CameraPoolJobItem, CameraPoolJobRequest, ConfigCameraPool
from .dto import CameraPoolJobItem, CameraPoolJobRequest
from .models import ConfigCameraPool

logger = logging.getLogger(__name__)
MAX_THREADS = 4
Expand All @@ -19,10 +19,9 @@ def __init__(self, config: ConfigCameraPool, nodes: list[CameraNode]):

# declare private props
self._primary_node: CameraNode = None
self._db: SimpleDb[CameraPoolJobItem] = None

# initialize priv props
self._db: SimpleDb[CameraPoolJobItem] = SimpleDb[CameraPoolJobItem]()
pass

def _identify_primary_node(self):
primary_nodes = [node for node in self._nodes if node.is_primary]
Expand Down Expand Up @@ -102,7 +101,7 @@ def setup_and_trigger_pool(self, camerapooljobrequest: CameraPoolJobRequest) ->
results = [future.result() for future in futures]

camerapooljobitem = CameraPoolJobItem(request=camerapooljobrequest, node_ids=[result["id"] for result in results])
self._db.add_item(camerapooljobitem) # TODO: decide if to keep track in a db or leave it to the user
logger.info(camerapooljobitem)

return camerapooljobitem

Expand Down
24 changes: 24 additions & 0 deletions wigglecam/connector/dto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import uuid
from dataclasses import dataclass, field


@dataclass
class CameraPoolJobRequest:
sequential: bool = False # sync or sequential each tick next node?
number_captures: int = 1


@dataclass
class CameraPoolJobItem:
request: CameraPoolJobRequest
id: uuid.UUID = field(default_factory=uuid.uuid4)
node_ids: list[uuid.UUID] = field(default_factory=list)


@dataclass
class NodeStatus:
description: str = None
can_connect: bool = None
is_healthy: bool = None
is_primary: bool = None
status: str = None
27 changes: 0 additions & 27 deletions wigglecam/connector/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import uuid
from dataclasses import dataclass, field

from pydantic import BaseModel, Field, HttpUrl

# class Calibration(BaseModel):
Expand All @@ -21,27 +18,3 @@ class ConfigCameraNode(BaseModel):

class ConfigCameraPool(BaseModel):
keep_node_copy: bool = False


@dataclass
class CameraPoolJobRequest:
sequential: bool = False # sync or sequential each tick next node?
number_captures: int = 1


@dataclass
class CameraPoolJobItem:
request: CameraPoolJobRequest
id: uuid.UUID = field(default_factory=uuid.uuid4)
node_ids: list[uuid.UUID] = field(default_factory=list)

def asdict(self) -> dict:
out = {
prop: getattr(self, prop)
for prop in dir(self)
if (
not prop.startswith("_") # no privates
and not callable(getattr(__class__, prop, None)) # no callables
)
}
return out
7 changes: 4 additions & 3 deletions wigglecam/routers/api/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from fastapi.responses import Response, StreamingResponse

from ...container import container
from ...services.acquisitionservice import CameraParameters
from ...services.dto import AcquisitionCameraParameters

logger = logging.getLogger(__name__)
router = APIRouter(
Expand Down Expand Up @@ -60,8 +60,9 @@ def still_camera():
) from exc


@router.get("/configure")
def configure_camera(parameters: CameraParameters):
@router.post("/configure")
def configure_camera(cameraparameters: AcquisitionCameraParameters):
print(cameraparameters)
raise NotImplementedError


Expand Down
7 changes: 0 additions & 7 deletions wigglecam/services/acquisitionservice.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import logging
import time
from dataclasses import dataclass
from importlib import import_module
from threading import Event, current_thread

Expand All @@ -13,12 +12,6 @@
logger = logging.getLogger(__name__)


@dataclass
class CameraParameters:
iso: int | None = None
shutter: int | None = None


class AcquisitionService(BaseService):
def __init__(self, config: ConfigSyncedAcquisition):
super().__init__()
Expand Down
3 changes: 2 additions & 1 deletion wigglecam/services/backends/cameras/abstractbackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ def __init__(self):
self._started_evt: Event = None
self._camera_thread: StoppableThread = None
self._ticker_thread: StoppableThread = None
self._adjust_cycle_counter: int = None
self._barrier: Barrier = None
self._current_timestamp_reference_in_queue: Queue[int] = None
self._current_timestampset: TimestampSet = None

# init
self._started_evt = Event()
self._adjust_cycle_counter: int = 0

def __repr__(self):
return f"{self.__class__}"
Expand Down Expand Up @@ -84,7 +86,6 @@ def _backend_align(self):
f"{self._current_timestampset.camera/1e6:.1f} / "
f"{timestamp_delta_ns/1e6:5.1f} / "
f"{adjust_amount_ns/1e6:5.1f}) ms"
# f"FrameDuration={round(picam_metadata['FrameDuration']/1e3,1)} ms "
)
else:
pass
Expand Down
15 changes: 15 additions & 0 deletions wigglecam/services/backends/cameras/dto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from dataclasses import dataclass


@dataclass
class BackendCameraMetadata:
iso: int
shutter: object
capture_time: str


@dataclass
class BackendCameraCapture:
timestamp_ns: int
frame: object
metadata: BackendCameraMetadata
10 changes: 6 additions & 4 deletions wigglecam/services/backends/cameras/virtualcamera.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from threading import BrokenBarrierError, Condition, current_thread

import numpy
from PIL import Image
from PIL import Image, ImageDraw

from ....utils.stoppablethread import StoppableThread
from ...config.models import ConfigBackendVirtualCamera
Expand Down Expand Up @@ -85,8 +85,6 @@ def encode_frame_to_image(self, frame, format: Formats) -> bytes:
raise NotImplementedError

def _produce_dummy_image(self) -> bytes:
from PIL import ImageDraw

offset_x = self._offset_x
offset_y = self._offset_y

Expand All @@ -103,13 +101,17 @@ def _produce_dummy_image(self) -> bytes:
random_image.paste(mask, (size // ellipse_divider + offset_x, size // ellipse_divider + offset_y), mask=mask)

random_image.save(byte_io, format="JPEG", quality=50)
return byte_io.getbuffer()

return byte_io.getvalue()
# getvalue (actual bytes copy) instead getbuffer (memoryview) to avoid Exception ignored in: <_io.BytesIO object at xxx>
# BufferError: Existing exports of data: object cannot be re-sized

def _producer_fun(self):
logger.debug("starting _producer_fun")

while not current_thread().stopped():
img, exposure_timestamp_ns = self._get_image()

try:
self._producer_queue.put_nowait((img, exposure_timestamp_ns))
except Full:
Expand Down
48 changes: 48 additions & 0 deletions wigglecam/services/dto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import uuid
from dataclasses import dataclass, field
from pathlib import Path

from .backends.cameras.dto import BackendCameraCapture


@dataclass
class JobRequest:
number_captures: int = 1
# TODO: maybe captures:list[bool]=[True] # True=capture, False=skip


@dataclass
class JobItem:
request: JobRequest

id: uuid.UUID = field(default_factory=uuid.uuid4)
# urls: list[str] = field(default_factory=list)
filepaths: list[Path] = field(default_factory=list)

# @property
# def is_finished(self) -> bool:
# return self.request.number_captures == len(self.filepaths) # if more this is also considered as error!

# def asdict(self) -> dict:
# out = {
# prop: getattr(self, prop)
# for prop in dir(self)
# if (
# not prop.startswith("_") # no privates
# and not callable(getattr(__class__, prop, None)) # no callables
# and not isinstance(getattr(self, prop), Path) # no path instances (not json.serializable)
# )
# }
# return out


@dataclass
class AcquisitionCapture:
seq: int
backendcapture: BackendCameraCapture


@dataclass
class AcquisitionCameraParameters:
iso: int | None = None
shutter: int | None = None
38 changes: 2 additions & 36 deletions wigglecam/services/jobservice.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import logging
import os
import uuid
from collections import namedtuple
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from threading import current_thread
Expand All @@ -12,6 +10,7 @@
from .acquisitionservice import AcquisitionService
from .baseservice import BaseService
from .config.models import ConfigJobConnected
from .dto import JobItem, JobRequest

logger = logging.getLogger(__name__)

Expand All @@ -22,37 +21,6 @@
PATH_ORIGINAL = DATA_PATH / "original"


@dataclass
class JobRequest:
number_captures: int = 1
# TODO: maybe captures:list[bool]=[True] # True=capture, False=skip


@dataclass
class JobItem:
request: JobRequest

id: uuid.UUID = field(default_factory=uuid.uuid4)
# urls: list[str] = field(default_factory=list)
filepaths: list[Path] = field(default_factory=list)

@property
def is_finished(self) -> bool:
return self.request.number_captures == len(self.filepaths) # if more this is also considered as error!

def asdict(self) -> dict:
out = {
prop: getattr(self, prop)
for prop in dir(self)
if (
not prop.startswith("_") # no privates
and not callable(getattr(__class__, prop, None)) # no callables
and not isinstance(getattr(self, prop), Path) # no path instances (not json.serializable)
)
}
return out


class JobService(BaseService):
def __init__(self, config: ConfigJobConnected, acquisition_service: AcquisitionService):
super().__init__()
Expand Down Expand Up @@ -104,8 +72,6 @@ def reset_job(self):
self._current_job = None

def trigger_execute_job(self):
# TODO: all this should run only on primary device! it's not validated, the connector needs to ensure to call the right device currently.
# maybe config can be changed in future and so also the _tirgger_out_thread is not started on secondary nodes.
self._acquisition_service.trigger_execute_job()

def _proc_job(self):
Expand All @@ -118,7 +84,7 @@ def _proc_job(self):
frames.append(
Captures(
i,
datetime.now().astimezone().strftime("%Y%m%d-%H%M%S-%f"),
datetime.now().astimezone().strftime("%Y%m%d-%H%M%S-%f"), # TODO: maybe we can in future use the actual time of capture.
self._acquisition_service.wait_for_hires_frame(),
)
)
Expand Down

0 comments on commit 4ba4de9

Please sign in to comment.