Skip to content

Commit 3a5f7ca

Browse files
CameraManager: no need for locks
1 parent d1ce153 commit 3a5f7ca

File tree

5 files changed

+37
-76
lines changed

5 files changed

+37
-76
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- App framework: simplify MANIFEST.JSON
1010
- AudioFlinger framework: simplify import, use singleton class
1111
- Create new SettingsActivity and SettingActivity framework so apps can easily add settings screens with just a few lines of code
12+
- Create CameraManager framework so apps can easily check whether there is a camera available etc.
1213
- Improve robustness by catching unhandled app exceptions
1314
- Improve robustness with custom exception that does not deinit() the TaskHandler
1415
- Improve robustness by removing TaskHandler callback that throws an uncaught exception

internal_filesystem/lib/mpos/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .activity_navigator import ActivityNavigator
1111
from .content.package_manager import PackageManager
1212
from .task_manager import TaskManager
13+
from . import camera_manager as CameraManager
1314

1415
# Common activities
1516
from .app.activities.chooser import ChooserActivity
@@ -64,7 +65,7 @@
6465
"Activity",
6566
"SharedPreferences",
6667
"ConnectivityManager", "DownloadManager", "WifiService", "AudioFlinger", "Intent",
67-
"ActivityNavigator", "PackageManager", "TaskManager",
68+
"ActivityNavigator", "PackageManager", "TaskManager", "CameraManager",
6869
# Common activities
6970
"ChooserActivity", "ViewActivity", "ShareActivity",
7071
"SettingActivity", "SettingsActivity", "CameraActivity",

internal_filesystem/lib/mpos/camera_manager.py

Lines changed: 16 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,6 @@
2222
Copyright (c) 2024 MicroPythonOS contributors
2323
"""
2424

25-
try:
26-
import _thread
27-
_lock = _thread.allocate_lock()
28-
except ImportError:
29-
_lock = None
3025

3126

3227
# Camera lens facing constants (matching Android Camera2 API)
@@ -105,22 +100,15 @@ def add_camera(camera):
105100
print(f"[CameraManager] Error: add_camera() requires Camera object, got {type(camera)}")
106101
return False
107102

108-
if _lock:
109-
_lock.acquire()
110-
111-
try:
112-
# Check if camera with same facing already exists
113-
for existing in _cameras:
114-
if existing.lens_facing == camera.lens_facing:
115-
print(f"[CameraManager] Warning: Camera with facing {camera.lens_facing} already registered")
116-
# Still add it (allow multiple cameras with same facing)
117-
118-
_cameras.append(camera)
119-
print(f"[CameraManager] Registered camera: {camera}")
120-
return True
121-
finally:
122-
if _lock:
123-
_lock.release()
103+
# Check if camera with same facing already exists
104+
for existing in _cameras:
105+
if existing.lens_facing == camera.lens_facing:
106+
print(f"[CameraManager] Warning: Camera with facing {camera.lens_facing} already registered")
107+
# Still add it (allow multiple cameras with same facing)
108+
109+
_cameras.append(camera)
110+
print(f"[CameraManager] Registered camera: {camera}")
111+
return True
124112

125113

126114
def get_cameras():
@@ -129,14 +117,7 @@ def get_cameras():
129117
Returns:
130118
list: List of Camera objects (copy of internal list)
131119
"""
132-
if _lock:
133-
_lock.acquire()
134-
135-
try:
136-
return _cameras.copy() if _cameras else []
137-
finally:
138-
if _lock:
139-
_lock.release()
120+
return _cameras.copy() if _cameras else []
140121

141122

142123
def get_camera_by_facing(lens_facing):
@@ -148,17 +129,10 @@ def get_camera_by_facing(lens_facing):
148129
Returns:
149130
Camera object or None if not found
150131
"""
151-
if _lock:
152-
_lock.acquire()
153-
154-
try:
155-
for camera in _cameras:
156-
if camera.lens_facing == lens_facing:
157-
return camera
158-
return None
159-
finally:
160-
if _lock:
161-
_lock.release()
132+
for camera in _cameras:
133+
if camera.lens_facing == lens_facing:
134+
return camera
135+
return None
162136

163137

164138
def has_camera():
@@ -167,14 +141,7 @@ def has_camera():
167141
Returns:
168142
bool: True if at least one camera available
169143
"""
170-
if _lock:
171-
_lock.acquire()
172-
173-
try:
174-
return len(_cameras) > 0
175-
finally:
176-
if _lock:
177-
_lock.release()
144+
return len(_cameras) > 0
178145

179146

180147
def get_camera_count():
@@ -183,14 +150,7 @@ def get_camera_count():
183150
Returns:
184151
int: Number of cameras
185152
"""
186-
if _lock:
187-
_lock.acquire()
188-
189-
try:
190-
return len(_cameras)
191-
finally:
192-
if _lock:
193-
_lock.release()
153+
return len(_cameras)
194154

195155

196156
# Initialize on module load

tests/test_audioflinger.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def setUp(self):
3434
# Reset volume to default before each test
3535
AudioFlinger.set_volume(70)
3636

37-
AudioFlinger.init(
37+
AudioFlinger(
3838
i2s_pins=self.i2s_pins,
3939
buzzer_instance=self.buzzer
4040
)
@@ -52,21 +52,21 @@ def test_initialization(self):
5252
def test_has_i2s(self):
5353
"""Test has_i2s() returns correct value."""
5454
# With I2S configured
55-
AudioFlinger.init(i2s_pins=self.i2s_pins, buzzer_instance=None)
55+
AudioFlinger(i2s_pins=self.i2s_pins, buzzer_instance=None)
5656
self.assertTrue(AudioFlinger.has_i2s())
5757

5858
# Without I2S configured
59-
AudioFlinger.init(i2s_pins=None, buzzer_instance=self.buzzer)
59+
AudioFlinger(i2s_pins=None, buzzer_instance=self.buzzer)
6060
self.assertFalse(AudioFlinger.has_i2s())
6161

6262
def test_has_buzzer(self):
6363
"""Test has_buzzer() returns correct value."""
6464
# With buzzer configured
65-
AudioFlinger.init(i2s_pins=None, buzzer_instance=self.buzzer)
65+
AudioFlinger(i2s_pins=None, buzzer_instance=self.buzzer)
6666
self.assertTrue(AudioFlinger.has_buzzer())
6767

6868
# Without buzzer configured
69-
AudioFlinger.init(i2s_pins=self.i2s_pins, buzzer_instance=None)
69+
AudioFlinger(i2s_pins=self.i2s_pins, buzzer_instance=None)
7070
self.assertFalse(AudioFlinger.has_buzzer())
7171

7272
def test_stream_types(self):
@@ -95,7 +95,7 @@ def test_volume_control(self):
9595
def test_no_hardware_rejects_playback(self):
9696
"""Test that no hardware rejects all playback requests."""
9797
# Re-initialize with no hardware
98-
AudioFlinger.init(i2s_pins=None, buzzer_instance=None)
98+
AudioFlinger(i2s_pins=None, buzzer_instance=None)
9999

100100
# WAV should be rejected (no I2S)
101101
result = AudioFlinger.play_wav("test.wav")
@@ -108,7 +108,7 @@ def test_no_hardware_rejects_playback(self):
108108
def test_i2s_only_rejects_rtttl(self):
109109
"""Test that I2S-only config rejects buzzer playback."""
110110
# Re-initialize with I2S only
111-
AudioFlinger.init(i2s_pins=self.i2s_pins, buzzer_instance=None)
111+
AudioFlinger(i2s_pins=self.i2s_pins, buzzer_instance=None)
112112

113113
# RTTTL should be rejected (no buzzer)
114114
result = AudioFlinger.play_rtttl("Test:d=4,o=5,b=120:c")
@@ -117,7 +117,7 @@ def test_i2s_only_rejects_rtttl(self):
117117
def test_buzzer_only_rejects_wav(self):
118118
"""Test that buzzer-only config rejects I2S playback."""
119119
# Re-initialize with buzzer only
120-
AudioFlinger.init(i2s_pins=None, buzzer_instance=self.buzzer)
120+
AudioFlinger(i2s_pins=None, buzzer_instance=self.buzzer)
121121

122122
# WAV should be rejected (no I2S)
123123
result = AudioFlinger.play_wav("test.wav")
@@ -142,7 +142,7 @@ def test_audio_focus_check_no_current_stream(self):
142142
def test_volume_default_value(self):
143143
"""Test that default volume is reasonable."""
144144
# After init, volume should be at default (70)
145-
AudioFlinger.init(i2s_pins=None, buzzer_instance=None)
145+
AudioFlinger(i2s_pins=None, buzzer_instance=None)
146146
self.assertEqual(AudioFlinger.get_volume(), 70)
147147

148148

@@ -162,7 +162,7 @@ def setUp(self):
162162
af._current_recording = None
163163
AudioFlinger.set_volume(70)
164164

165-
AudioFlinger.init(
165+
AudioFlinger(
166166
i2s_pins=self.i2s_pins_with_mic,
167167
buzzer_instance=self.buzzer
168168
)
@@ -173,17 +173,17 @@ def tearDown(self):
173173

174174
def test_has_microphone_with_sd_in(self):
175175
"""Test has_microphone() returns True when sd_in pin is configured."""
176-
AudioFlinger.init(i2s_pins=self.i2s_pins_with_mic, buzzer_instance=None)
176+
AudioFlinger(i2s_pins=self.i2s_pins_with_mic, buzzer_instance=None)
177177
self.assertTrue(AudioFlinger.has_microphone())
178178

179179
def test_has_microphone_without_sd_in(self):
180180
"""Test has_microphone() returns False when sd_in pin is not configured."""
181-
AudioFlinger.init(i2s_pins=self.i2s_pins_no_mic, buzzer_instance=None)
181+
AudioFlinger(i2s_pins=self.i2s_pins_no_mic, buzzer_instance=None)
182182
self.assertFalse(AudioFlinger.has_microphone())
183183

184184
def test_has_microphone_no_i2s(self):
185185
"""Test has_microphone() returns False when no I2S is configured."""
186-
AudioFlinger.init(i2s_pins=None, buzzer_instance=self.buzzer)
186+
AudioFlinger(i2s_pins=None, buzzer_instance=self.buzzer)
187187
self.assertFalse(AudioFlinger.has_microphone())
188188

189189
def test_is_recording_initially_false(self):
@@ -192,15 +192,14 @@ def test_is_recording_initially_false(self):
192192

193193
def test_record_wav_no_microphone(self):
194194
"""Test that record_wav() fails when no microphone is configured."""
195-
AudioFlinger.init(i2s_pins=self.i2s_pins_no_mic, buzzer_instance=None)
195+
AudioFlinger(i2s_pins=self.i2s_pins_no_mic, buzzer_instance=None)
196196
result = AudioFlinger.record_wav("test.wav")
197-
self.assertFalse(result)
197+
self.assertFalse(result, "record_wav() fails when no microphone is configured")
198198

199199
def test_record_wav_no_i2s(self):
200-
"""Test that record_wav() fails when no I2S is configured."""
201-
AudioFlinger.init(i2s_pins=None, buzzer_instance=self.buzzer)
200+
AudioFlinger(i2s_pins=None, buzzer_instance=self.buzzer)
202201
result = AudioFlinger.record_wav("test.wav")
203-
self.assertFalse(result)
202+
self.assertFalse(result, "record_wav() should fail when no I2S is configured")
204203

205204
def test_stop_with_no_recording(self):
206205
"""Test that stop() can be called when nothing is recording."""

tests/test_camera_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
import unittest
23
import sys
34
import os
@@ -295,4 +296,3 @@ def test_task_usage_pattern_no_camera(self):
295296

296297
self.assertFalse(has_camera)
297298

298-

0 commit comments

Comments
 (0)