diff --git a/carwatch/listen.py b/carwatch/listen.py index 8351af0..4442b09 100644 --- a/carwatch/listen.py +++ b/carwatch/listen.py @@ -430,9 +430,14 @@ def _speak(text: str) -> bool: # Bond can exist while the A2DP link is down; connect is cheap # when already connected (car-speak.sh does the same). if _bt_pcm_mac("a2dpsrc/sink") != car: - subprocess.run(["bluetoothctl", "connect", car], - capture_output=True, timeout=10) - time.sleep(2) + # An absent car makes connect hang to the timeout; that is + # a normal answer here, not a reason to drop the reply. + try: + subprocess.run(["bluetoothctl", "connect", car], + capture_output=True, timeout=10) + time.sleep(2) + except subprocess.TimeoutExpired: + pass # Only a LIVE link counts. The saved MAC outlives the car: on the # kitchen table (26 Sep, Jabra) every answer went to the absent # car's A2DP and the speakerphone on the desk stayed silent. diff --git a/scripts/restart-when-quiet.sh b/scripts/restart-when-quiet.sh index e5afd0f..10eb23c 100755 --- a/scripts/restart-when-quiet.sh +++ b/scripts/restart-when-quiet.sh @@ -39,4 +39,9 @@ sleep 1 systemctl daemon-reload systemctl enable --now carwatch-chat carwatch-presence systemctl restart $UNITS +# The voice listener is optional (USB mic installs only), so it rides along +# as try-restart: restarted when running, skipped when absent. Left out, it +# kept executing pre-update listen.py (26 Sep: #71 merged and pulled, the +# Jabra still silent until a hand restart). +systemctl try-restart carwatch-listen 2>/dev/null || true echo "restart-when-quiet: restarted $UNITS after $(( $(date +%s) - start ))s" diff --git a/tests/test_speak_route.py b/tests/test_speak_route.py index d85c21f..2e5a692 100644 --- a/tests/test_speak_route.py +++ b/tests/test_speak_route.py @@ -50,6 +50,29 @@ def run(cmd, **kw): def test_absent_car_falls_back_to_usb(self): self.assertEqual(self._route(None), ["plughw:0,0"]) + def test_connect_timeout_still_falls_back_to_usb(self): + # 26 Sep on vadelma: connect to the absent car hung past 10 s and the + # TimeoutExpired aborted the whole reply. + played = [] + + def run(cmd, **kw): + if cmd[0] == "bluetoothctl": + raise self.L.subprocess.TimeoutExpired(cmd, 10) + if cmd[0] == "aplay": + played.append(cmd[cmd.index("-D") + 1]) + return mock.Mock(returncode=0) + + from carwatch import voiceroom + with mock.patch.object(voiceroom, "tts_wav", return_value=self.wav), \ + mock.patch.object(self.L, "_car_a2dp_mac", return_value=CAR), \ + mock.patch.object(self.L, "_bt_pcm_mac", return_value=None), \ + mock.patch.object(self.L, "_usb_audio_device", return_value="plughw:0,0"), \ + mock.patch.object(self.L, "_echo_tail_sec", return_value=0.0), \ + mock.patch.object(self.L.subprocess, "run", side_effect=run), \ + mock.patch.object(self.L.time, "sleep"): + self.assertTrue(self.L._speak("hello")) + self.assertEqual(played, ["plughw:0,0"]) + def test_connected_car_still_wins(self): self.assertEqual(self._route(CAR), [f"bluealsa:DEV={CAR},PROFILE=a2dp"])