Skip to content

Commit f4248ee

Browse files
committed
Improve webcam logging
1 parent c39f81c commit f4248ee

File tree

4 files changed

+83
-35
lines changed

4 files changed

+83
-35
lines changed

example/virtual_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def __init__(self, *args, **kwargs):
7272
self.printer.tool_count = 1
7373
self.printer.tool().material_count = 4
7474

75-
self.set_camera_uri(URL("virtual://localhost"))
75+
self.camera_uri = URL("virtual://localhost")
7676
self.printer.webcam_info.connected = True
7777

7878
for i, mat in enumerate(self.printer.materials0):

simplyprint_ws_client/shared/camera/controller.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import threading
23
from typing import final, Optional, Callable
34

@@ -18,6 +19,7 @@ class CameraController(SyncStoppable, Synchronized):
1819

1920
protocol: BaseCameraProtocol
2021
pause_timeout: Optional[int]
22+
logger: logging.Logger
2123

2224
_frame_cb: Callable[[Optional[FrameT]], None]
2325
_pause_timer: Optional[threading.Timer] = None
@@ -28,12 +30,14 @@ def __init__(
2830
frame_cb: Callable[[Optional[FrameT]], None],
2931
protocol: BaseCameraProtocol,
3032
pause_timeout: Optional[int] = None,
33+
logger: logging.Logger = logging.getLogger(__name__),
3134
):
3235
SyncStoppable.__init__(self)
3336
Synchronized.__init__(self)
3437

3538
self.protocol = protocol
3639
self.pause_timeout = pause_timeout
40+
self.logger = logger
3741
self._frame_cb = frame_cb
3842

3943
def __del__(self):
@@ -102,11 +106,11 @@ def _read_loop(self):
102106
def _exception_handler(self, f: Callable):
103107
try:
104108
f()
105-
106109
except CameraProtocolConnectionError:
107110
self._frame_cb(None)
108111
except CameraProtocolInvalidState:
109112
self._frame_cb(None)
110113
except Exception as e:
111114
_ = e
115+
self.logger.debug("Failed to read from camera: %s", e)
112116
self._frame_cb(None)

simplyprint_ws_client/shared/camera/mixin.py

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import asyncio
22
import base64
33
import datetime
4+
import logging
5+
import os
46
from typing import Optional, Literal, TypeVar
57

68
from yarl import URL
@@ -24,42 +26,72 @@ class ClientCameraMixin(Client[_T]):
2426
_camera_pool: Optional[CameraPool] = None
2527
_camera_uri: Optional[URL] = None
2628
_camera_handle: Optional[CameraHandle] = None
29+
_camera_status: Literal["ok", "new", "err"] = "ok"
2730
_camera_max_cache_age: Optional[datetime.timedelta] = None
2831
_camera_pause_timeout: Optional[int] = None
2932
_camera_debug: bool = False
33+
_camera_logger: logging.Logger = logging.getLogger(__name__)
3034
_stream_lock: CancelableLock
3135
_stream_setup: asyncio.Event
3236
_request_count: int = 0
3337

3438
def initialize_camera_mixin(
35-
self,
36-
camera_pool: Optional[CameraPool] = None,
37-
pause_timeout: Optional[int] = None,
38-
max_cache_age: Optional[datetime.timedelta] = None,
39-
camera_debug=False,
40-
**_kwargs,
39+
self,
40+
camera_pool: Optional[CameraPool] = None,
41+
pause_timeout: Optional[int] = None,
42+
max_cache_age: Optional[datetime.timedelta] = None,
43+
camera_debug: Optional[bool] = None,
44+
**_kwargs,
4145
):
4246
self._camera_pool = camera_pool
4347
self._stream_lock = CancelableLock()
4448
self._stream_setup = asyncio.Event()
4549
self._camera_pause_timeout = pause_timeout
4650
self._camera_max_cache_age = max_cache_age
47-
self._camera_debug = camera_debug
51+
self._camera_debug = (
52+
"SIMPLYPRINT_DEBUG_CAMERA" in os.environ
53+
if camera_debug is None
54+
else camera_debug
55+
)
56+
self._camera_logger = self.logger.getChild("camera")
57+
self._camera_logger.setLevel(logging.DEBUG if self._camera_debug else logging.INFO)
58+
59+
@property
60+
def camera_status(self) -> Literal["ok", "new", "err"]:
61+
"""Get the camera status."""
62+
return self._camera_status
63+
64+
@property
65+
def camera_uri(self) -> Optional[URL]:
66+
"""Get the camera URI."""
67+
return self._camera_uri
4868

49-
def set_camera_uri(self, uri: Optional[URL] = None) -> Literal["err", "ok", "new"]:
69+
@camera_uri.setter
70+
def camera_uri(self, uri: Optional[URL] = None):
5071
"""Returns whether it has changed the camera URI"""
5172
if self._camera_pool is None:
52-
return "err"
73+
self._camera_status = "err"
74+
self._camera_logger.debug(
75+
f"Dropped camera URI {uri} because no camera pool is available."
76+
)
77+
return
5378

5479
# If the camera URI is the same, don't recreate the camera.
5580
if self._camera_uri == uri and self._camera_handle:
56-
return "ok"
81+
self._camera_status = "ok"
82+
self._camera_logger.debug(
83+
f"Camera URI {uri} is the same as the current one, not changing."
84+
)
85+
return
5786

5887
self._camera_uri = uri
5988

60-
# Clear out previous camera (if URI is different)
89+
# Clear out the previous camera (if URI is different)
6190
if self._camera_handle:
6291
self._camera_handle.stop()
92+
self._camera_logger.debug(
93+
f"Cleared previous camera handle ID {self._camera_handle.id}."
94+
)
6395
self._camera_handle = None
6496
self.event_loop.call_soon_threadsafe(self._stream_setup.clear)
6597

@@ -80,10 +112,13 @@ def set_camera_uri(self, uri: Optional[URL] = None) -> Literal["err", "ok", "new
80112
if not self.printer.webcam_info.connected:
81113
self.printer.webcam_info.connected = True
82114

83-
return "new"
115+
self._camera_status = "new"
116+
self._camera_logger.debug(
117+
f"Set new camera URI to {uri} with handle ID {self._camera_handle.id}, status is now {self._camera_status}."
118+
)
84119

85120
def __del__(self):
86-
self.set_camera_uri(None)
121+
self.camera_uri = None
87122

88123
async def on_stream_on(self):
89124
if not self._camera_handle:
@@ -109,10 +144,10 @@ def _before_webcam_snapshot(self, data: WebcamSnapshotDemandData):
109144
self._request_count += 1
110145

111146
async def on_webcam_snapshot(
112-
self,
113-
data: WebcamSnapshotDemandData = WebcamSnapshotDemandData(),
114-
attempt=0,
115-
retry_timeout=5,
147+
self,
148+
data: WebcamSnapshotDemandData = WebcamSnapshotDemandData(),
149+
attempt=0,
150+
retry_timeout=5,
116151
):
117152
if not self._camera_handle:
118153
await self._stream_setup.wait()
@@ -130,27 +165,28 @@ async def on_webcam_snapshot(
130165
# Empty frame or none.
131166
if not frame:
132167
if attempt <= 3:
133-
self.logger.debug(
168+
self._camera_logger.debug(
134169
f"Failed to get frame, retrying in {retry_timeout} seconds"
135170
)
136171
await asyncio.sleep(retry_timeout)
137172
await self.on_webcam_snapshot(data, attempt + 1, retry_timeout)
138173
else:
139-
self.logger.debug("Failed to get frame, giving up.")
174+
self._camera_logger.debug(
175+
f"Failed to get frame, giving up. Used camera handle id {self._camera_handle.id}."
176+
)
140177

141178
return
142179

143-
if self._camera_debug:
144-
self.logger.debug(
145-
f"Received frame from camera with size {len(frame) if frame else 0} bytes "
146-
f"with an fps of {self._camera_handle.fps or 'N/A'} in "
147-
f"{datetime.datetime.now() - st}."
148-
)
180+
self._camera_logger.debug(
181+
f"Received frame from camera with size {len(frame) if frame else 0} bytes "
182+
f"with an fps of {self._camera_handle.fps or 'N/A'} in "
183+
f"{datetime.datetime.now() - st} from camera handle id {self._camera_handle.id}."
184+
)
149185

150186
# Capture snapshot events and send them to the API
151187
if is_snapshot_event:
152188
await SimplyPrintApi.post_snapshot(data.id, frame, endpoint=data.endpoint)
153-
self.logger.debug(f"Posted snapshot to API with id {data.id}")
189+
self._camera_logger.debug(f"Posted snapshot to API with id {data.id}")
154190
return
155191

156192
# Mark the webcam as connected if it's not already.

simplyprint_ws_client/shared/camera/pool.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import functools
22
import logging
33
import multiprocessing
4-
import os
5-
import sys
64
import threading
75
import time
86
from concurrent.futures.thread import ThreadPoolExecutor
7+
from logging.handlers import RotatingFileHandler
98
from typing import final, List, Dict, Tuple, Optional, Type
109

1110
from yarl import URL
@@ -25,6 +24,7 @@
2524
from .handle import CameraHandle
2625
from ..utils.stoppable import ProcessStoppable, StoppableProcess
2726
from ..utils.synchronized import Synchronized
27+
from ...const import APP_DIRS
2828

2929

3030
@final
@@ -99,7 +99,7 @@ def run(self):
9999
try:
100100
self._run()
101101
except KeyboardInterrupt:
102-
pass
102+
logging.info("Exiting on keyboard interrupt")
103103
except Exception as e:
104104
logging.error("Error", exc_info=e)
105105

@@ -108,14 +108,22 @@ def _run(self):
108108
self.instances = {}
109109

110110
logging.basicConfig(
111-
level=logging.DEBUG
112-
if os.environ.get("SIMPLYPRINT_DEBUG_CAMERA", False)
113-
else logging.INFO,
111+
level=logging.DEBUG,
114112
format="%(asctime)s [%(process)d] %(message)s",
115113
datefmt="%H:%M:%S",
116-
handlers=[logging.StreamHandler(stream=sys.stdout)],
114+
handlers=[
115+
RotatingFileHandler(
116+
filename=APP_DIRS.user_log_path / "camera_worker.log",
117+
mode="a",
118+
maxBytes=10 * 1024 * 1024,
119+
backupCount=3,
120+
)
121+
],
122+
force=True,
117123
)
118124

125+
logging.debug("Camera worker started")
126+
119127
with ThreadPoolExecutor(thread_name_prefix="CameraWorkerProcess") as tp:
120128
while not self.is_stopped():
121129
msg: Optional[Request] = self.command_queue.get()

0 commit comments

Comments
 (0)