Skip to content

Commit c9848e3

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 4eb7e1b + 14d2de6 commit c9848e3

File tree

6 files changed

+109
-15
lines changed

6 files changed

+109
-15
lines changed

.github/workflows/docker-github-dev-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: DEV - build & publish Arduino dev containers
1+
name: DEV - build & publish Arduino dev-latest containers
22

33
on:
44
workflow_dispatch:

src/arduino/app_bricks/video_objectdetection/brick_compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ services:
1313
volumes:
1414
- "${CUSTOM_MODEL_PATH:-/home/arduino/.arduino-bricks/ei-models/}:${CUSTOM_MODEL_PATH:-/home/arduino/.arduino-bricks/ei-models/}"
1515
- "/run/udev:/run/udev"
16-
command: ["--model-file", "${EI_OBJ_DETECTION_MODEL:-/models/ootb/ei/yolo-x-nano.eim}", "--dont-print-predictions", "--mode", "streaming", "--force-target", "--preview-original-resolution", "--camera", "${VIDEO_DEVICE:-/dev/video1}"]
16+
command: ["--model-file", "${EI_OBJ_DETECTION_MODEL:-/models/ootb/ei/yolo-x-nano.eim}", "--dont-print-predictions", "--mode", "streaming", "--preview-original-resolution", "--camera", "${VIDEO_DEVICE:-/dev/video1}"]
1717
healthcheck:
1818
test: [ "CMD-SHELL", "wget -q --spider http://ei-video-obj-detection-runner:4912 || exit 1" ]
1919
interval: 2s

src/arduino/app_utils/audio.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class SineGenerator:
1818
sample_rate (int): Audio sample rate in Hz.
1919
attack (float): Attack time for amplitude smoothing in seconds.
2020
release (float): Release time for amplitude smoothing in seconds.
21+
glide (float): Glide time for frequency smoothing in seconds.
2122
"""
2223

2324
def __init__(self, sample_rate: int):
@@ -31,6 +32,7 @@ def __init__(self, sample_rate: int):
3132
# envelope parameters (attack/release in seconds)
3233
self.attack = 0.01
3334
self.release = 0.03
35+
self.glide = 0.02
3436

3537
# reusable buffers
3638
self._buf_N = 0
@@ -88,17 +90,19 @@ def set_state(self, state: dict) -> None:
8890
if "freq_last" in state:
8991
self._freq_last = float(state["freq_last"])
9092

91-
def set_envelope_params(self, attack: float, release: float) -> None:
93+
def set_envelope_params(self, attack: float, release: float, glide: float) -> None:
9294
"""Update attack and release envelope parameters.
9395
9496
Args:
9597
attack (float): Attack time in seconds (time to rise to target
9698
amplitude when increasing amplitude).
9799
release (float): Release time in seconds (time to fall to target
98100
amplitude when decreasing amplitude).
101+
glide (float): Glide time in seconds (time to reach target frequency).
99102
"""
100103
self.attack = float(max(0.0, attack))
101104
self.release = float(max(0.0, release))
105+
self.glide = float(max(0.0, glide))
102106

103107
def generate_block(self, freq: float, amp_target: float, block_dur: float, master_volume: float):
104108
"""Generate a block of float32 audio samples.
@@ -149,10 +153,29 @@ def generate_block(self, freq: float, amp_target: float, block_dur: float, maste
149153
envelope[:] = np.linspace(amp_current, next_amp, N, dtype=np.float32)
150154
amp_current = float(envelope[-1])
151155

152-
# oscillator
153-
phase_incr = 2.0 * math.pi * float(freq) / float(self.sample_rate)
156+
# frequency glide (portamento)
157+
freq_current = float(self._freq_last)
158+
freq_target = float(freq)
159+
glide = float(self.glide)
154160
phase_incs = self._buf_phase_incs[:N]
155-
phase_incs.fill(phase_incr)
161+
162+
if glide > 0.0 and freq_current != freq_target:
163+
# Apply glide smoothing over time
164+
frac = min(1.0, block_dur / glide)
165+
next_freq = freq_current + (freq_target - freq_current) * frac
166+
167+
# Linear interpolation within block
168+
freq_ramp = np.linspace(freq_current, next_freq, N, dtype=np.float32)
169+
phase_incs[:] = 2.0 * math.pi * freq_ramp / float(self.sample_rate)
170+
171+
freq_current = float(next_freq)
172+
else:
173+
# No glide or already at target
174+
phase_incr = 2.0 * math.pi * freq_target / float(self.sample_rate)
175+
phase_incs.fill(phase_incr)
176+
freq_current = freq_target
177+
178+
# oscillator (phase accumulation)
156179
np.cumsum(phase_incs, dtype=np.float32, out=phases)
157180
phases += self._phase
158181
self._phase = float(phases[-1] % (2.0 * math.pi))
@@ -168,6 +191,6 @@ def generate_block(self, freq: float, amp_target: float, block_dur: float, maste
168191

169192
# update state
170193
self._amp_current = amp_current
171-
self._freq_last = float(freq)
194+
self._freq_last = freq_current
172195

173196
return samples

src/arduino/app_utils/bridge.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -516,10 +516,10 @@ def _handle_msg(self, msg: list):
516516
try:
517517
if msg_type == 0: # Request: [0, msgid, method, params]
518518
if len(msg) != 4:
519-
raise ValueError("Invalid RPC request: expected length 4")
519+
raise ValueError(f"Invalid RPC request: expected length 4, got {len(msg)}")
520520
_, msgid, method, params = msg
521521
if not isinstance(params, (list, tuple)):
522-
raise ValueError("Invalid RPC request params: expected array/tuple")
522+
raise ValueError("Invalid RPC request params: expected array or tuple")
523523

524524
method_name = self._decode_method(method)
525525

@@ -538,7 +538,7 @@ def _handle_msg(self, msg: list):
538538

539539
elif msg_type == 1: # Response: [1, msgid, error, result]
540540
if len(msg) != 4:
541-
raise ValueError("Invalid RPC response: expected length 4")
541+
raise ValueError(f"Invalid RPC response: expected length 4, got {len(msg)}")
542542
_, msgid, error, result = msg
543543
if error and (not isinstance(error, list) or len(error) < 2):
544544
raise ValueError("Invalid error format in RPC response")
@@ -559,11 +559,11 @@ def _handle_msg(self, msg: list):
559559
else:
560560
on_result([GENERIC_ERR, "Unknown error occurred."])
561561
else:
562-
on_error([GENERIC_ERR, f"Response for unknown msgid {msgid} received."])
562+
logger.warning(f"Response for unknown msgid {msgid} received.")
563563

564564
elif msg_type == 2: # Notification: [2, method, params]
565565
if len(msg) != 3:
566-
raise ValueError("Invalid RPC notification: expected length 3")
566+
raise ValueError(f"Invalid RPC notification: expected length 3, got {len(msg)}")
567567
_, method, params = msg
568568
if not isinstance(params, (list, tuple)):
569569
raise ValueError("Invalid RPC notification params: expected array or tuple")

tests/arduino/app_utils/bridge/test_unit_common.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ def setUp(self):
1515

1616
# Patch dependencies
1717
# Mock the logger used by ClientServer
18-
patch("arduino.app_utils.bridge.logger", MagicMock()).start()
18+
self.mock_logger = MagicMock()
19+
self.logger_patcher = patch("arduino.app_utils.bridge.logger", self.mock_logger)
20+
self.logger_patcher.start()
1921

2022
# Mock the socket instance that will be created
2123
self.mock_socket_instance = MagicMock()
@@ -32,5 +34,6 @@ def setUp(self):
3234

3335
def tearDown(self):
3436
"""This method is called after each test and cleans up the patched dependencies."""
37+
self.logger_patcher.stop()
3538
self.socket_patcher.stop()
3639
self.threading_patcher.stop()

tests/arduino/app_utils/bridge/test_unit_handle_msg.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,58 @@
99

1010

1111
class TestHandleMsg(UnitTest):
12+
def test_empty_msg(self):
13+
"""Test handling of an empty message."""
14+
client = ClientServer()
15+
client._handle_msg([])
16+
self.mock_logger.warning.assert_called_once_with("Invalid RPC message received (must be a non-empty list).")
17+
self.mock_logger.error.assert_not_called()
18+
19+
def test_unknown_msg_type(self):
20+
"""Test handling of an unknown message type."""
21+
client = ClientServer()
22+
client._handle_msg([99, 1, None, "result"]) # Msg type 99 does not exist
23+
self.mock_logger.warning.assert_called_once_with("Invalid RPC message type received: 99")
24+
self.mock_logger.error.assert_not_called()
25+
26+
def test_unknown_msg_id(self):
27+
"""Test handling of an unknown message id."""
28+
client = ClientServer()
29+
client._handle_msg([1, 9999, None, "result"]) # Msg id 9999 does not exist
30+
self.mock_logger.warning.assert_called_once_with("Response for unknown msgid 9999 received.")
31+
self.mock_logger.error.assert_not_called()
32+
33+
def test_malformed_messages(self):
34+
"""Test handling of malformed messages."""
35+
client = ClientServer()
36+
37+
client._handle_msg([0, 1, "method", [0, 1], "extra field"]) # Malformed payload
38+
self.mock_logger.warning.assert_not_called()
39+
self.mock_logger.error.assert_called_once_with("Message validation error: Invalid RPC request: expected length 4, got 5")
40+
self.mock_logger.reset_mock()
41+
client._handle_msg([0, 1, "method", 1]) # Malformed params
42+
self.mock_logger.warning.assert_not_called()
43+
self.mock_logger.error.assert_called_once_with("Message validation error: Invalid RPC request params: expected array or tuple")
44+
self.mock_logger.reset_mock()
45+
46+
client._handle_msg([1, 1, None, "result", "extra field"]) # Malformed payload
47+
self.mock_logger.warning.assert_not_called()
48+
self.mock_logger.error.assert_called_once_with("Message validation error: Invalid RPC response: expected length 4, got 5")
49+
self.mock_logger.reset_mock()
50+
client._handle_msg([1, 1, 42, "result"]) # Malformed error
51+
self.mock_logger.warning.assert_not_called()
52+
self.mock_logger.error.assert_called_once_with("Message validation error: Invalid error format in RPC response")
53+
self.mock_logger.reset_mock()
54+
55+
client._handle_msg([2, 1, [0, 1], "extra field"]) # Malformed payload
56+
self.mock_logger.warning.assert_not_called()
57+
self.mock_logger.error.assert_called_once_with("Message validation error: Invalid RPC notification: expected length 3, got 4")
58+
self.mock_logger.reset_mock()
59+
client._handle_msg([2, 1, 42]) # Malformed params
60+
self.mock_logger.warning.assert_not_called()
61+
self.mock_logger.error.assert_called_once_with("Message validation error: Invalid RPC notification params: expected array or tuple")
62+
self.mock_logger.reset_mock()
63+
1264
def test_handle_msg_request(self):
1365
"""Test handling of an incoming request message."""
1466
client = ClientServer()
@@ -27,6 +79,22 @@ def test_handle_msg_request(self):
2779
handler_mock.assert_called_once_with(*params)
2880
client._send_response.assert_called_once_with(msgid, None, "handled")
2981

82+
def test_handle_msg_request_handler_fail(self):
83+
"""Test handling of a request for a method that fails running its handler."""
84+
client = ClientServer()
85+
client._send_response = MagicMock()
86+
87+
request_msg = [0, 111, "failing_method", []]
88+
client.handlers["failing_method"] = MagicMock(side_effect=ValueError("Handler failed"))
89+
90+
client._handle_msg(request_msg)
91+
92+
client._send_response.assert_called_once()
93+
args, _ = client._send_response.call_args
94+
self.assertEqual(args[0], 111) # msgid
95+
self.assertIsInstance(args[1], ValueError) # error
96+
self.assertIsNone(args[2]) # result
97+
3098
def test_handle_msg_request_method_not_found(self):
3199
"""Test handling of a request for a method that is not found."""
32100
client = ClientServer()
@@ -79,7 +147,7 @@ def test_handle_msg_response(self):
79147
on_error_mock.assert_not_called()
80148
self.assertNotIn(msgid, client.callbacks) # Callback should be removed
81149

82-
def test_handle_msg_response_function_not_found(self):
150+
def test_handle_msg_generic_error_response(self):
83151
"""Test handling of an incoming error response message."""
84152
client = ClientServer()
85153

@@ -100,7 +168,7 @@ def test_handle_msg_response_function_not_found(self):
100168
on_error_mock.assert_called_once_with(result_error)
101169
self.assertNotIn(msgid, client.callbacks) # Callback should be removed
102170

103-
def test_handle_msg_response_already_provided_error(self):
171+
def test_handle_msg_method_exists_error_response(self):
104172
"""Test handling of an incoming error response message that signals a method is already provided."""
105173
client = ClientServer()
106174

0 commit comments

Comments
 (0)