mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-10-28 00:53:45 +08:00
BlueALSA: Bump to v5.0.0
This commit is contained in:
@@ -19,6 +19,7 @@ a.out
|
||||
bin/
|
||||
!openpilot/starpilot/galaxy/bin/
|
||||
!openpilot/starpilot/soloist/pulseaudio/bin/
|
||||
!openpilot/starpilot/soloist/bluealsa/bin/
|
||||
|
||||
# created at launch for comma hardware PYTHONPATH (PC uses editable installs via pyproject.toml)
|
||||
/msgq
|
||||
@@ -47,6 +48,7 @@ bin/
|
||||
!openpilot/tools/replay/libmedia_mux.so
|
||||
!openpilot/starpilot/soloist/pulseaudio/lib/*.so
|
||||
!openpilot/starpilot/soloist/pulseaudio/modules/*.so
|
||||
!openpilot/starpilot/soloist/bluealsa/lib/*.so
|
||||
*.a
|
||||
st[0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z][0-9A-Za-z]
|
||||
*.unchunked
|
||||
|
||||
@@ -43,17 +43,13 @@ page only after they are updated.
|
||||
|
||||
## Runtime behavior
|
||||
|
||||
The cached AGNOS image's BlueALSA service starts `/usr/bin/bluealsa -p a2dp-source`.
|
||||
`phone_profile.py`, invoked through noninteractive sudo on the device, temporarily
|
||||
adds `a2dp-sink` using its own drop-in under `/run/systemd/system/bluealsa.service.d`.
|
||||
Only BlueALSA is restarted, once on entry and once on exit; Bluetooth bonds,
|
||||
bluetoothd, adapter power/identity and HFP registration are not changed. Unknown
|
||||
service commands and externally modified drop-ins are rejected. The override is
|
||||
removed on orderly shutdown/source change, recovered at the next supervisor start
|
||||
after an abrupt shutdown, and disappears at reboot. If the supervisor is killed
|
||||
without restarting, the receiving profile can remain advertised until reboot;
|
||||
its capture worker exits on parent death. Bluetooth music may briefly disconnect
|
||||
while BlueALSA restarts. Radio-off is respected; this feature never powers it on.
|
||||
The checkout bundles BlueALSA 5.0.0's daemon and matching PCM/CTL plugins for
|
||||
phone reception and Bluetooth output. `phone_profile.py` uses temporary `/run`
|
||||
overrides; the system's BlueALSA 4.1.1 installation stays intact. Phone capture
|
||||
uses `hwcompat "silence"` for continuity across input interruptions. Private ALSA
|
||||
configs select the matching plugins and explicitly preserve software volume.
|
||||
See [the bundle documentation](bluealsa/README.md) for build inputs, tests, and
|
||||
the old-commit rollback contract (restart music/openpilot or reboot after checkout).
|
||||
|
||||
Only the selected paired phone is captured. The existing trusted pairing policy
|
||||
still determines which phones may establish Bluetooth connections; this feature
|
||||
@@ -61,7 +57,7 @@ does not alter trust or erase bonds. `arecord` opens an ALSA plug PCM for that
|
||||
phone, converting to signed 16-bit 48 kHz stereo. A separate Python worker writes
|
||||
to the private server using AGNOS's `libpulse-simple.so.0`, with bounded buffers.
|
||||
The supervisor retries disconnects without repeatedly restarting Bluetooth.
|
||||
No extra native binaries are bundled or built for this feature.
|
||||
The versioned BlueALSA bundle and private PulseAudio server are included with the checkout.
|
||||
|
||||
## Validation
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.starpilot.soloist import bluealsa_runtime
|
||||
from openpilot.starpilot.soloist.paths import Layout, private_dir
|
||||
from openpilot.starpilot.soloist.preferences import ADDRESS, Preferences
|
||||
|
||||
@@ -53,7 +54,8 @@ def alsa_config(address: str, base: Path = SYSTEM_ALSA_CONFIG) -> str:
|
||||
raise BackendUnavailable('Choose a Bluetooth speaker or car first')
|
||||
if any(character in str(base) for character in '\n\r<>"'):
|
||||
raise BackendUnavailable('Invalid system ALSA configuration path')
|
||||
return (f'<{base}>\n\npcm.{PCM_NAME} {{\n type bluealsa\n device "{address}"\n profile "a2dp"\n' +
|
||||
return (f'<{base}>\n' + bluealsa_runtime.alsa_plugins() +
|
||||
f'\npcm.{PCM_NAME} {{\n type bluealsa\n device "{address}"\n profile "a2dp"\n softvol "true"\n' +
|
||||
' hint { show on description "StarPilot Spotify output" }\n}\n')
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build/
|
||||
@@ -0,0 +1 @@
|
||||
sha256:8e0457d4150457e582a95dab9d2d13fc35c437610ec03ac8729d7b57d73d59d7
|
||||
@@ -0,0 +1,245 @@
|
||||
adduser 3.137ubuntu1
|
||||
alsa-utils 1.2.9-1ubuntu5
|
||||
apt 2.8.3
|
||||
autoconf 2.71-3
|
||||
automake 1:1.16.5-1.3ubuntu1
|
||||
autotools-dev 20220109.1
|
||||
base-files 13ubuntu10.4
|
||||
base-passwd 3.6.3build1
|
||||
bash 5.2.21-2ubuntu4
|
||||
binutils 2.42-4ubuntu2.10
|
||||
binutils-aarch64-linux-gnu 2.42-4ubuntu2.10
|
||||
binutils-common:arm64 2.42-4ubuntu2.10
|
||||
bsdutils 1:2.39.3-9ubuntu6.6
|
||||
build-essential 12.10ubuntu1
|
||||
bzip2 1.0.8-5.1ubuntu0.1
|
||||
ca-certificates 20260816ubuntu0.24.04.1
|
||||
check:arm64 0.15.2-2build1
|
||||
coreutils 9.4-3ubuntu6.2
|
||||
cpp 4:13.2.0-7ubuntu1
|
||||
cpp-13 13.3.0-6ubuntu2~24.04.1
|
||||
cpp-13-aarch64-linux-gnu 13.3.0-6ubuntu2~24.04.1
|
||||
cpp-aarch64-linux-gnu 4:13.2.0-7ubuntu1
|
||||
dash 0.5.12-6ubuntu5
|
||||
dbus 1.14.10-4ubuntu4.1
|
||||
dbus-bin 1.14.10-4ubuntu4.1
|
||||
dbus-daemon 1.14.10-4ubuntu4.1
|
||||
dbus-session-bus-common 1.14.10-4ubuntu4.1
|
||||
dbus-system-bus-common 1.14.10-4ubuntu4.1
|
||||
debconf 1.5.86ubuntu1
|
||||
debianutils 5.17build1
|
||||
diffutils 1:3.10-1build1
|
||||
dpkg 1.22.6ubuntu6.6
|
||||
dpkg-dev 1.22.6ubuntu6.6
|
||||
e2fsprogs 1.47.0-2.4~exp1ubuntu4.1
|
||||
file 1:5.45-3build1
|
||||
findutils 4.9.0-5build1
|
||||
g++ 4:13.2.0-7ubuntu1
|
||||
g++-13 13.3.0-6ubuntu2~24.04.1
|
||||
g++-13-aarch64-linux-gnu 13.3.0-6ubuntu2~24.04.1
|
||||
g++-aarch64-linux-gnu 4:13.2.0-7ubuntu1
|
||||
gcc 4:13.2.0-7ubuntu1
|
||||
gcc-13 13.3.0-6ubuntu2~24.04.1
|
||||
gcc-13-aarch64-linux-gnu 13.3.0-6ubuntu2~24.04.1
|
||||
gcc-13-base:arm64 13.3.0-6ubuntu2~24.04.1
|
||||
gcc-14-base:arm64 14.2.0-4ubuntu2~24.04.1
|
||||
gcc-aarch64-linux-gnu 4:13.2.0-7ubuntu1
|
||||
git 1:2.43.0-1ubuntu7.4
|
||||
git-man 1:2.43.0-1ubuntu7.4
|
||||
gpgv 2.4.4-2ubuntu17.4
|
||||
grep 3.11-4build1
|
||||
gzip 1.12-1ubuntu3.2
|
||||
hostname 3.23+nmu2ubuntu2
|
||||
init-system-helpers 1.66ubuntu1
|
||||
kmod 31+20240202-2ubuntu7.2
|
||||
libacl1:arm64 2.3.2-1build1.1
|
||||
libapparmor1:arm64 4.0.1really4.0.1-0ubuntu0.24.04.9
|
||||
libapt-pkg6.0t64:arm64 2.8.3
|
||||
libasan8:arm64 14.2.0-4ubuntu2~24.04.1
|
||||
libasound2-data 1.2.11-1ubuntu0.3
|
||||
libasound2-dev:arm64 1.2.11-1ubuntu0.3
|
||||
libasound2t64:arm64 1.2.11-1ubuntu0.3
|
||||
libassuan0:arm64 2.5.6-1build1
|
||||
libatomic1:arm64 14.2.0-4ubuntu2~24.04.1
|
||||
libatopology2t64:arm64 1.2.11-1ubuntu0.3
|
||||
libattr1:arm64 1:2.5.2-1build1.1
|
||||
libaudit-common 1:3.1.2-2.1build1.1
|
||||
libaudit1:arm64 1:3.1.2-2.1build1.1
|
||||
libbinutils:arm64 2.42-4ubuntu2.10
|
||||
libblkid-dev:arm64 2.39.3-9ubuntu6.6
|
||||
libblkid1:arm64 2.39.3-9ubuntu6.6
|
||||
libbluetooth-dev:arm64 5.72-0ubuntu5.8
|
||||
libbluetooth3:arm64 5.72-0ubuntu5.8
|
||||
libbrotli1:arm64 1.1.0-2build2
|
||||
libbsd-dev:arm64 0.12.1-1build1.1
|
||||
libbsd0:arm64 0.12.1-1build1.1
|
||||
libbz2-1.0:arm64 1.0.8-5.1ubuntu0.1
|
||||
libc-bin 2.39-0ubuntu8.9
|
||||
libc-dev-bin 2.39-0ubuntu8.9
|
||||
libc6:arm64 2.39-0ubuntu8.9
|
||||
libc6-dev:arm64 2.39-0ubuntu8.9
|
||||
libcap-ng0:arm64 0.8.4-2build2
|
||||
libcap2:arm64 1:2.66-5ubuntu2.4
|
||||
libcc1-0:arm64 14.2.0-4ubuntu2~24.04.1
|
||||
libcom-err2:arm64 1.47.0-2.4~exp1ubuntu4.1
|
||||
libcrypt-dev:arm64 1:4.4.36-4build1
|
||||
libcrypt1:arm64 1:4.4.36-4build1
|
||||
libctf-nobfd0:arm64 2.42-4ubuntu2.10
|
||||
libctf0:arm64 2.42-4ubuntu2.10
|
||||
libcurl3t64-gnutls:arm64 8.5.0-2ubuntu10.16
|
||||
libdb5.3t64:arm64 5.3.28+dfsg2-7
|
||||
libdbus-1-3:arm64 1.14.10-4ubuntu4.1
|
||||
libdbus-1-dev:arm64 1.14.10-4ubuntu4.1
|
||||
libdebconfclient0:arm64 0.271ubuntu3
|
||||
libdpkg-perl 1.22.6ubuntu6.6
|
||||
libelf1t64:arm64 0.190-1.1ubuntu0.1
|
||||
liberror-perl 0.17029-2
|
||||
libexpat1:arm64 2.6.1-2ubuntu0.6
|
||||
libext2fs2t64:arm64 1.47.0-2.4~exp1ubuntu4.1
|
||||
libffi-dev:arm64 3.4.6-1build1
|
||||
libffi8:arm64 3.4.6-1build1
|
||||
libfftw3-single3:arm64 3.3.10-1ubuntu3
|
||||
libgcc-13-dev:arm64 13.3.0-6ubuntu2~24.04.1
|
||||
libgcc-s1:arm64 14.2.0-4ubuntu2~24.04.1
|
||||
libgcrypt20:arm64 1.10.3-2ubuntu0.1
|
||||
libgdbm-compat4t64:arm64 1.23-5.1build1
|
||||
libgdbm6t64:arm64 1.23-5.1build1
|
||||
libgirepository-2.0-0:arm64 2.80.0-6ubuntu3.10
|
||||
libglib2.0-0t64:arm64 2.80.0-6ubuntu3.10
|
||||
libglib2.0-bin 2.80.0-6ubuntu3.10
|
||||
libglib2.0-data 2.80.0-6ubuntu3.10
|
||||
libglib2.0-dev:arm64 2.80.0-6ubuntu3.10
|
||||
libglib2.0-dev-bin 2.80.0-6ubuntu3.10
|
||||
libgmp10:arm64 2:6.3.0+dfsg-2ubuntu6.1
|
||||
libgnutls30t64:arm64 3.8.3-1.1ubuntu3.6
|
||||
libgomp1:arm64 14.2.0-4ubuntu2~24.04.1
|
||||
libgpg-error0:arm64 1.47-3build2.1
|
||||
libgprofng0:arm64 2.42-4ubuntu2.10
|
||||
libgssapi-krb5-2:arm64 1.20.1-6ubuntu2.10
|
||||
libhogweed6t64:arm64 3.9.1-2.2build1.1
|
||||
libhwasan0:arm64 14.2.0-4ubuntu2~24.04.1
|
||||
libidn2-0:arm64 2.3.7-2build1.1
|
||||
libisl23:arm64 0.26-3build1.1
|
||||
libitm1:arm64 14.2.0-4ubuntu2~24.04.1
|
||||
libjansson4:arm64 2.14-2build2
|
||||
libk5crypto3:arm64 1.20.1-6ubuntu2.10
|
||||
libkeyutils1:arm64 1.6.3-3build1
|
||||
libkmod2:arm64 31+20240202-2ubuntu7.2
|
||||
libkrb5-3:arm64 1.20.1-6ubuntu2.10
|
||||
libkrb5support0:arm64 1.20.1-6ubuntu2.10
|
||||
libldacbt-abr-dev:arm64 2.0.2.3+git20200429+ed310a0-4ubuntu2
|
||||
libldacbt-abr2:arm64 2.0.2.3+git20200429+ed310a0-4ubuntu2
|
||||
libldacbt-enc-dev:arm64 2.0.2.3+git20200429+ed310a0-4ubuntu2
|
||||
libldacbt-enc2:arm64 2.0.2.3+git20200429+ed310a0-4ubuntu2
|
||||
libldap2:arm64 2.6.10+dfsg-0ubuntu0.24.04.1
|
||||
liblsan0:arm64 14.2.0-4ubuntu2~24.04.1
|
||||
liblz4-1:arm64 1.9.4-1build1.1
|
||||
liblzma5:arm64 5.6.1+really5.4.5-1ubuntu0.4
|
||||
libmagic-mgc 1:5.45-3build1
|
||||
libmagic1t64:arm64 1:5.45-3build1
|
||||
libmd-dev:arm64 1.1.0-2build1.1
|
||||
libmd0:arm64 1.1.0-2build1.1
|
||||
libmount-dev:arm64 2.39.3-9ubuntu6.6
|
||||
libmount1:arm64 2.39.3-9ubuntu6.6
|
||||
libmpc3:arm64 1.3.1-1build1.1
|
||||
libmpfr6:arm64 4.2.1-1build1.1
|
||||
libncursesw6:arm64 6.4+20240113-1ubuntu2.1
|
||||
libnettle8t64:arm64 3.9.1-2.2build1.1
|
||||
libnghttp2-14:arm64 1.59.0-1ubuntu0.4
|
||||
libnpth0t64:arm64 1.6-3.1build1
|
||||
libp11-kit0:arm64 0.25.3-4ubuntu2.1
|
||||
libpam-modules:arm64 1.5.3-5ubuntu5.6
|
||||
libpam-modules-bin 1.5.3-5ubuntu5.6
|
||||
libpam-runtime 1.5.3-5ubuntu5.6
|
||||
libpam0g:arm64 1.5.3-5ubuntu5.6
|
||||
libpcre2-16-0:arm64 10.42-4ubuntu2.1
|
||||
libpcre2-32-0:arm64 10.42-4ubuntu2.1
|
||||
libpcre2-8-0:arm64 10.42-4ubuntu2.1
|
||||
libpcre2-dev:arm64 10.42-4ubuntu2.1
|
||||
libpcre2-posix3:arm64 10.42-4ubuntu2.1
|
||||
libperl5.38t64:arm64 5.38.2-3.2ubuntu0.6
|
||||
libpkgconf3:arm64 1.8.1-2build1
|
||||
libproc2-0:arm64 2:4.0.4-4ubuntu3.2
|
||||
libpsl5t64:arm64 0.21.2-1.1build1
|
||||
libpython3-stdlib:arm64 3.12.3-0ubuntu2.1
|
||||
libpython3.12-minimal:arm64 3.12.3-1ubuntu0.17
|
||||
libpython3.12-stdlib:arm64 3.12.3-1ubuntu0.17
|
||||
libreadline8t64:arm64 8.2-4build1
|
||||
librtmp1:arm64 2.4+20151223.gitfa8646d.1-2build7
|
||||
libsamplerate0:arm64 0.2.2-4build1
|
||||
libsamplerate0-dev:arm64 0.2.2-4build1
|
||||
libsasl2-2:arm64 2.1.28+dfsg1-5ubuntu3.1
|
||||
libsasl2-modules-db:arm64 2.1.28+dfsg1-5ubuntu3.1
|
||||
libsbc-dev:arm64 2.0-1build1
|
||||
libsbc1:arm64 2.0-1build1
|
||||
libseccomp2:arm64 2.5.5-1ubuntu3.1
|
||||
libselinux1:arm64 3.5-2ubuntu2.1
|
||||
libselinux1-dev:arm64 3.5-2ubuntu2.1
|
||||
libsemanage-common 3.5-1build5
|
||||
libsemanage2:arm64 3.5-1build5
|
||||
libsepol-dev:arm64 3.5-2build1
|
||||
libsepol2:arm64 3.5-2build1
|
||||
libsframe1:arm64 2.42-4ubuntu2.10
|
||||
libsmartcols1:arm64 2.39.3-9ubuntu6.6
|
||||
libsqlite3-0:arm64 3.45.1-1ubuntu2.8
|
||||
libss2:arm64 1.47.0-2.4~exp1ubuntu4.1
|
||||
libssh-4:arm64 0.10.6-2ubuntu0.5
|
||||
libssl3t64:arm64 3.0.13-0ubuntu3.16
|
||||
libstdc++-13-dev:arm64 13.3.0-6ubuntu2~24.04.1
|
||||
libstdc++6:arm64 14.2.0-4ubuntu2~24.04.1
|
||||
libsubunit-dev:arm64 1.4.2-3build1
|
||||
libsubunit0:arm64 1.4.2-3build1
|
||||
libsystemd0:arm64 255.4-1ubuntu8.17
|
||||
libtasn1-6:arm64 4.19.0-3ubuntu0.24.04.2
|
||||
libtinfo6:arm64 6.4+20240113-1ubuntu2.1
|
||||
libtool 2.4.7-7build1
|
||||
libtsan2:arm64 14.2.0-4ubuntu2~24.04.1
|
||||
libubsan1:arm64 14.2.0-4ubuntu2~24.04.1
|
||||
libudev1:arm64 255.4-1ubuntu8.17
|
||||
libunistring5:arm64 1.1-2build1.1
|
||||
libuuid1:arm64 2.39.3-9ubuntu6.6
|
||||
libxxhash0:arm64 0.8.2-2build1
|
||||
libzstd1:arm64 1.5.5+dfsg2-2build1.1
|
||||
linux-libc-dev:arm64 6.8.0-147.147
|
||||
login 1:4.13+dfsg1-4ubuntu3.2
|
||||
logsave 1.47.0-2.4~exp1ubuntu4.1
|
||||
lto-disabled-list 47
|
||||
m4 1.4.19-4build1
|
||||
make 4.3-4.1build2
|
||||
mawk 1.3.4.20240123-1build1
|
||||
media-types 10.1.0
|
||||
mount 2.39.3-9ubuntu6.6
|
||||
ncurses-base 6.4+20240113-1ubuntu2.1
|
||||
ncurses-bin 6.4+20240113-1ubuntu2.1
|
||||
netbase 6.4
|
||||
openssl 3.0.13-0ubuntu3.16
|
||||
passwd 1:4.13+dfsg1-4ubuntu3.2
|
||||
patch 2.7.6-7build3
|
||||
perl 5.38.2-3.2ubuntu0.6
|
||||
perl-base 5.38.2-3.2ubuntu0.6
|
||||
perl-modules-5.38 5.38.2-3.2ubuntu0.6
|
||||
pkg-config:arm64 1.8.1-2build1
|
||||
pkgconf:arm64 1.8.1-2build1
|
||||
pkgconf-bin 1.8.1-2build1
|
||||
procps 2:4.0.4-4ubuntu3.2
|
||||
python3 3.12.3-0ubuntu2.1
|
||||
python3-minimal 3.12.3-0ubuntu2.1
|
||||
python3-packaging 24.0-1
|
||||
python3.12 3.12.3-1ubuntu0.17
|
||||
python3.12-minimal 3.12.3-1ubuntu0.17
|
||||
readline-common 8.2-4build1
|
||||
rpcsvc-proto 1.4.2-0ubuntu7
|
||||
sed 4.9-2ubuntu0.24.04.1
|
||||
sensible-utils 0.0.22
|
||||
sgml-base 1.31
|
||||
sysvinit-utils 3.08-6ubuntu3
|
||||
tar 1.35+dfsg-3ubuntu0.4
|
||||
tzdata 2026e-0ubuntu0.24.04.1
|
||||
ubuntu-keyring 2023.11.28.1
|
||||
unminimize 0.2.1
|
||||
util-linux 2.39.3-9ubuntu6.6
|
||||
uuid-dev:arm64 2.39.3-9ubuntu6.6
|
||||
xml-core 0.19
|
||||
xz-utils 5.6.1+really5.4.5-1ubuntu0.4
|
||||
zlib1g:arm64 1:1.3.dfsg-3.1ubuntu2.2
|
||||
zlib1g-dev:arm64 1:1.3.dfsg-3.1ubuntu2.2
|
||||
@@ -0,0 +1,6 @@
|
||||
FROM ubuntu:24.04@sha256:33ceb71981b602c1a7443a53469e4dba065f7503eab3078a2d7a57a2ab987517
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
build-essential autoconf automake libtool pkg-config git ca-certificates \
|
||||
libasound2-dev libbluetooth-dev libdbus-1-dev libglib2.0-dev libsbc-dev \
|
||||
libldacbt-enc-dev libldacbt-abr-dev libbsd-dev check dbus alsa-utils libsamplerate0-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -0,0 +1,125 @@
|
||||
# BlueALSA 5.0.0 for StarPilot music
|
||||
|
||||
This is a checkout-local Linux/ARM64 bundle of upstream BlueALSA **5.0.0**, commit
|
||||
`1935d6dcb8975f2d7a51aaafe61538d157224623`. It replaces the abandoned 4.1.1 SBC
|
||||
concealment experiment. There are **no custom decoder patches** in this bundle.
|
||||
|
||||
`bin/bluealsad`, `bin/bluealsactl`, `bin/bluealsa-aplay`, and both ALSA plugins in
|
||||
`lib/` must be committed together with `VERSION`, `UPSTREAM_COMMIT`, and
|
||||
`SHA256SUMS`. The feature validates checksums and ARM64 ELF headers before
|
||||
activating the daemon. Build scratch files under `build/` are ignored.
|
||||
|
||||
## Audio integration
|
||||
|
||||
The phone profile helper temporarily selects `bluealsad` through the existing
|
||||
`bluealsa.service`. Both phone capture and Spotify/Bluetooth output use explicit
|
||||
paths to the bundled PCM/CTL plugins in their process-local ALSA configuration.
|
||||
No global plugin search path, ALSA config, D-Bus policy, or system binary changes.
|
||||
The D-Bus service remains `org.bluealsa`; the bundled plugins and tool use its
|
||||
5.0 `Rate` property instead of 4.1's `Sampling`. No application code reads that
|
||||
property directly. Existing diagnostics still query the same systemd unit.
|
||||
|
||||
Phone capture enables `hwcompat "silence"` to maintain capture across transport
|
||||
inactivity and short input interruptions. It inserts silence; it does not
|
||||
reconstruct missing music or eliminate interference. Playback leaves `hwcompat`
|
||||
at its default so a disconnected output is not silently consumed. Both paths
|
||||
explicitly set `softvol "true"` to preserve the previous software-volume mode
|
||||
instead of adopting 5.0's new native-volume default. Initial device volume state
|
||||
is separate from 4.1; the user's existing 4.1 state is never rewritten.
|
||||
|
||||
The build retains SBC reception, SBC/LDAC transmission, and oFono support. The
|
||||
launcher explicitly enables SBC and LDAC. AAC, Opus, LHDC, and ASHA are not enabled
|
||||
by this upgrade. `bluealsa-aplay` is built with adaptive resampling available,
|
||||
but the `arecord` relay does not use it. The existing `Nice=-5` policy is retained;
|
||||
no real-time scheduling priority is added over driving processes.
|
||||
|
||||
5.0 persistent state is compiled to `/data/starpilot/bluealsa5/lib/bluealsa`.
|
||||
The launcher creates its parent before starting the daemon. This isolates it
|
||||
from `/var/lib/bluealsa`, preserving 4.1 volume/mute state for rollback.
|
||||
|
||||
## Rollback contract
|
||||
|
||||
**Guaranteed rollback: check out the old commit, then reboot.** `/run` is cleared at
|
||||
boot, so the stock 4.1.1 daemon returns with no further steps. Restarting
|
||||
music/openpilot instead is best-effort: it works only if the old supervisor's
|
||||
startup release runs (see below). Checking out files alone cannot replace a daemon
|
||||
already running in memory.
|
||||
|
||||
* `/usr/bin/bluealsa`, system ALSA plugins, system service, and 4.1 persistent
|
||||
state are never overwritten.
|
||||
* `90-starpilot-phone.conf` remains **byte-for-byte compatible with the old
|
||||
helper**. 5.0 selection is a separate `95-starpilot-bluealsa5.conf` in `/run`.
|
||||
* The launcher is copied into `/run/systemd/system/` before activation. It is
|
||||
self-contained and remains available after Git removes the new checkout files.
|
||||
* Checking out a pre-upgrade commit removes the tracked `VERSION` and bundle.
|
||||
On the old supervisor's normal startup, its existing release code removes
|
||||
`90-starpilot-phone.conf` and restarts the unit. The retained launcher sees the
|
||||
absent version marker, removes the 5.0 override and itself, reloads systemd,
|
||||
then execs the stock 4.1 daemon with the original source profile. Old capture
|
||||
and playback code generates configs using the system 4.1 plugins again.
|
||||
* A reboot independently clears all `/run` overrides, including for much older
|
||||
commits without the phone-profile startup cleanup. No migration command is
|
||||
required for that rollback path.
|
||||
* A failed activation removes the new override and restores the prior profile
|
||||
configuration before trying the system daemon again. Unknown overrides are
|
||||
refused rather than overwritten.
|
||||
|
||||
The integration test performs a real temporary Git checkout back to an older
|
||||
commit, then runs the frozen pre-upgrade helper from `3434023608`. Service calls
|
||||
are simulated so the test cannot change the host or a comma. It verifies the
|
||||
resulting stock daemon command and removal of both temporary overrides/launcher.
|
||||
This is local lifecycle verification, not an on-device audio acceptance test.
|
||||
|
||||
## Build and verification
|
||||
|
||||
Read `docs/how-to/laptop-device-build.md` first. This narrowly scoped bundle build
|
||||
uses Linux/ARM64 Ubuntu 24.04 (AGNOS's userspace generation); it does not invoke
|
||||
the full openpilot artifact build or deploy anything. The base image is pinned;
|
||||
`BUILD_IMAGE`, `BUILD_PACKAGES.txt`, and `UPSTREAM_COMMIT` record build inputs.
|
||||
Package versions are recorded, not guaranteed to remain available from apt.
|
||||
|
||||
```sh
|
||||
git clone --branch v5.0.0 https://github.com/arkq/bluez-alsa.git /tmp/bluealsa-5
|
||||
openpilot/starpilot/soloist/bluealsa/build.sh --setup
|
||||
openpilot/starpilot/soloist/bluealsa/build.sh /tmp/bluealsa-5
|
||||
.venv/bin/python -m pytest openpilot/starpilot/soloist/tests -q
|
||||
```
|
||||
|
||||
The setup command downloads build dependencies. The build itself uses
|
||||
`--network none --pull never`, exports only the pinned committed source, runs
|
||||
upstream `make check`, builds matching daemon/plugins/tools, verifies ARM64 ELF
|
||||
and dynamic dependencies in the build container, and writes checksums. It does
|
||||
not build inside the mounted Git worktree, avoiding container ownership issues.
|
||||
|
||||
`UPSTREAM_TEST_RESULTS.txt` records the upstream suite (19 programs, zero skips).
|
||||
This includes capture/playback PCM and each `hwcompat` mode. The local integration
|
||||
suite covers bundle integrity, capture/playback config, profile ownership,
|
||||
activation failure, Spotify output lifecycle, and old-commit rollback. The actual
|
||||
packaged plugin was also loaded by `arecord` and `aplay` using generated StarPilot
|
||||
configs in an isolated D-Bus session: it reached the expected absent-service error
|
||||
without config or symbol errors. No physical device was used for these checks.
|
||||
|
||||
Device facts verified read-only on the comma (2026-10-21): `bluealsa.service` runs as
|
||||
root with no `ProtectSystem`/`NoNewPrivileges`, so the launcher can write `/data`,
|
||||
edit `/run/systemd/system` and `daemon-reload`; every bundled artifact resolves all
|
||||
shared libraries (`ldd`, including LDAC and samplerate); the private v5 PCM plugin is
|
||||
the one ALSA loads through the generated config (it overrides the system
|
||||
`/etc/alsa/conf.d/20-bluealsa.conf` type). A v5 plugin talking to the stock 4.1
|
||||
daemon fails (`Incorrect variant for 'Volume'`), which is why Spotify Bluetooth
|
||||
output refuses to start (`audio_unavailable`) when v5 activation fails instead of
|
||||
falling back to a mismatched pair. The v5 daemon itself has not yet been started on
|
||||
the device.
|
||||
|
||||
Behavior changes to be aware of: while Spotify plays to a Bluetooth output the
|
||||
daemon runs with the same `a2dp-sink` profile as phone reception, so the comma is
|
||||
advertised as a music receiver; and a player restart releases and re-acquires the
|
||||
profile, briefly restarting BlueALSA (and so Bluetooth music).
|
||||
|
||||
Remaining hardware acceptance after deployment: reconnect a phone, pause/resume,
|
||||
exercise Android Auto and Bluetooth output, and confirm perceived dropout
|
||||
behavior. Local tests do not establish audible smoothness or RF improvement.
|
||||
|
||||
Upstream references:
|
||||
- https://github.com/arkq/bluez-alsa/blob/v5.0.0/NEWS.md
|
||||
- https://github.com/arkq/bluez-alsa/blob/v5.0.0/doc/bluealsa-plugins.7.rst
|
||||
- https://github.com/arkq/bluez-alsa/blob/v5.0.0/doc/bluealsad.8.rst
|
||||
@@ -0,0 +1,5 @@
|
||||
bcb5e1a1a8462a5aaaf1fa198b09c9fb9bd26cb8d658b052462a0336655cf7b7 bin/bluealsa-aplay
|
||||
e9d30ca988de2856d4bc98e1b1b2b81a461fe021c008fd15ccaf0faa9925c128 bin/bluealsactl
|
||||
60c91e60d36b55d670e886170539e4ee7223d2f2153fafafe8e570cbf2b18672 bin/bluealsad
|
||||
d1cf683fe82ff1f3c7268fe2614ada5a05a63e3ceb2603689c08740b5618f118 lib/libasound_module_ctl_bluealsa.so
|
||||
3372f48574b56b320424ee8e3f9319aa920967d16538c6ff77f3ac94174b19c4 lib/libasound_module_pcm_bluealsa.so
|
||||
@@ -0,0 +1 @@
|
||||
1935d6dcb8975f2d7a51aaafe61538d157224623
|
||||
@@ -0,0 +1,14 @@
|
||||
==========================================
|
||||
BlueALSA v5.0.0: test/test-suite.log
|
||||
==========================================
|
||||
|
||||
# TOTAL: 19
|
||||
# PASS: 19
|
||||
# SKIP: 0
|
||||
# XFAIL: 0
|
||||
# FAIL: 0
|
||||
# XPASS: 0
|
||||
# ERROR: 0
|
||||
|
||||
.. contents:: :depth: 2
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
5.0.0
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# Explicit, isolated ARM64 build. Does not install or restart anything on a comma.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
image=starpilot-bluealsa-builder:24.04
|
||||
source_commit=1935d6dcb8975f2d7a51aaafe61538d157224623
|
||||
if [[ ${1:-} == --setup ]]; then
|
||||
docker build --platform linux/arm64 -t "$image" .
|
||||
exit
|
||||
fi
|
||||
if [[ $# != 1 || ! -d $1/.git ]]; then
|
||||
echo 'Usage: build.sh --setup | build.sh /path/to/bluez-alsa-v5.0.0' >&2
|
||||
exit 2
|
||||
fi
|
||||
source_dir=$(cd "$1" && pwd)
|
||||
[[ $(git -C "$source_dir" rev-parse HEAD) == "$source_commit" ]] || { echo 'Wrong BlueALSA source revision' >&2; exit 1; }
|
||||
# Export only committed upstream files. Local changes are never silently bundled.
|
||||
mkdir -p build
|
||||
archive=$(mktemp)
|
||||
trap 'rm -f "$archive"' EXIT
|
||||
git -C "$source_dir" archive "$source_commit" > "$archive"
|
||||
cp "$archive" build/upstream.tar
|
||||
docker run --rm --platform linux/arm64 --pull never --network none \
|
||||
--mount "type=bind,src=$PWD,dst=/package" "$image" bash -euc '
|
||||
mkdir /tmp/source
|
||||
cd /tmp/source
|
||||
tar xf /package/build/upstream.tar
|
||||
autoreconf --install >/tmp/autoreconf.log 2>&1 || { cat /tmp/autoreconf.log; exit 1; }
|
||||
./configure --localstatedir=/data/starpilot/bluealsa5 --disable-debug --disable-manpages --enable-ldac --enable-ofono --enable-test --with-libsamplerate > /tmp/configure.log 2>&1 || { cat /tmp/configure.log; exit 1; }
|
||||
make -j4 >/tmp/make.log 2>&1 || { cat /tmp/make.log; exit 1; }
|
||||
make -j4 check >/tmp/check.log 2>&1 || { cat test/test-suite.log /tmp/check.log; exit 1; }
|
||||
mkdir -p /package/bin /package/lib /package/licenses
|
||||
cp src/bluealsad src/bluealsactl/bluealsactl utils/aplay/bluealsa-aplay /package/bin/
|
||||
cp src/asound/.libs/libasound_module_{pcm,ctl}_bluealsa.so /package/lib/
|
||||
strip /package/bin/* /package/lib/*
|
||||
/package/bin/bluealsad --version
|
||||
/package/bin/bluealsad --help > /package/build/help.txt
|
||||
: > /package/build/dependencies.txt
|
||||
for artifact in /package/bin/* /package/lib/*; do
|
||||
readelf -h "$artifact" | grep "Machine:.*AArch64"
|
||||
ldd "$artifact" >> /package/build/dependencies.txt
|
||||
done
|
||||
! grep -q "not found" /package/build/dependencies.txt
|
||||
dpkg-query -W > /package/build/build-packages.txt
|
||||
cp /tmp/configure.log /tmp/check.log /package/build/
|
||||
cp test/test-suite.log /package/build/
|
||||
cp test/test-suite.log /package/UPSTREAM_TEST_RESULTS.txt
|
||||
cp /package/build/build-packages.txt /package/BUILD_PACKAGES.txt
|
||||
cp LICENSE /package/licenses/BlueALSA.txt
|
||||
cp LICENSES/* /package/licenses/
|
||||
cd /package
|
||||
sha256sum bin/* lib/* > SHA256SUMS
|
||||
'
|
||||
printf '%s\n' "$source_commit" > UPSTREAM_COMMIT
|
||||
printf '%s\n' '5.0.0' > VERSION
|
||||
docker image inspect --format '{{.Id}}' "$image" > BUILD_IMAGE
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Copied to /run before activation; intentionally has no checkout imports.
|
||||
|
||||
An older checkout removes VERSION. Its existing phone_profile.release() restarts
|
||||
bluealsa, so this retained launcher removes the v5 override and execs stock v4.
|
||||
A reboot also removes /run overrides. Neither path modifies /usr or v4 state.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def select(bundle: Path, legacy: Path, override: Path, reload):
|
||||
marker = bundle / 'VERSION'
|
||||
if marker.exists():
|
||||
if marker.read_text().strip() != '5.0.0':
|
||||
raise RuntimeError('Unsupported bundled BlueALSA version')
|
||||
binary = bundle / 'bin' / 'bluealsad'
|
||||
if not binary.is_file():
|
||||
raise RuntimeError('Incomplete BlueALSA 5 bundle')
|
||||
# Keep the existing scheduling policy; no new real-time priority over driving.
|
||||
return [str(binary), '-p', 'a2dp-source', '-p', 'a2dp-sink', '-c', 'SBC', '-c', 'LDAC']
|
||||
override.unlink(missing_ok=True)
|
||||
(legacy.parent.parent / 'starpilot-bluealsa-launch.py').unlink(missing_ok=True)
|
||||
reload()
|
||||
argv = ['/usr/bin/bluealsa', '-p', 'a2dp-source']
|
||||
if legacy.exists():
|
||||
argv += ['-p', 'a2dp-sink']
|
||||
return argv
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
argv = select(*(Path(p) for p in sys.argv[1:]),
|
||||
reload=lambda: subprocess.run(['/usr/bin/systemctl', 'daemon-reload'], check=True, timeout=15))
|
||||
if argv[0] != '/usr/bin/bluealsa':
|
||||
Path('/data/starpilot/bluealsa5/lib').mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
os.execv(argv[0], argv)
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2016-2025 BlueALSA developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,121 @@
|
||||
Creative Commons Legal Code
|
||||
|
||||
CC0 1.0 Universal
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2016-2025 BlueALSA developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Checkout-local BlueALSA 5 bundle and reversible /run service selection."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent / 'bluealsa'
|
||||
ARTIFACTS = ('bin/bluealsad', 'bin/bluealsactl', 'bin/bluealsa-aplay',
|
||||
'lib/libasound_module_pcm_bluealsa.so', 'lib/libasound_module_ctl_bluealsa.so')
|
||||
|
||||
|
||||
def validate(root: Path | None = None) -> None:
|
||||
root = root or ROOT
|
||||
if (root / 'VERSION').read_text().strip() != '5.0.0':
|
||||
raise RuntimeError('BlueALSA 5.0.0 bundle is missing')
|
||||
records = dict(line.split(maxsplit=1)[::-1] for line in (root / 'SHA256SUMS').read_text().splitlines())
|
||||
for name in ARTIFACTS:
|
||||
data = (root / name).read_bytes()
|
||||
if records.get(name) != hashlib.sha256(data).hexdigest():
|
||||
raise RuntimeError(f'BlueALSA artifact checksum failed: {name}')
|
||||
if data[:6] != b'\x7fELF\x02\x01' or data[18:20] != b'\xb7\x00':
|
||||
raise RuntimeError(f'BlueALSA artifact is not ARM64: {name}')
|
||||
if name.startswith('bin/') and not os.access(root / name, os.X_OK):
|
||||
raise RuntimeError(f'BlueALSA executable is not executable: {name}')
|
||||
|
||||
|
||||
def alsa_plugins(root: Path | None = None) -> str:
|
||||
"""Explicit library paths affect this ALSA config only, never system defaults."""
|
||||
root = root or ROOT
|
||||
result = ''
|
||||
for kind in ('pcm', 'ctl'):
|
||||
library = str(root / 'lib' / f'libasound_module_{kind}_bluealsa.so')
|
||||
if any(c in library for c in '\n\r"\\'):
|
||||
raise RuntimeError('Invalid BlueALSA plugin path')
|
||||
result += f'{kind}_type.bluealsa {{ lib "{library}" }}\n'
|
||||
return result
|
||||
|
||||
|
||||
def paths(legacy: Path) -> tuple[Path, Path]:
|
||||
return legacy.with_name('95-starpilot-bluealsa5.conf'), legacy.parent.parent / 'starpilot-bluealsa-launch.py'
|
||||
|
||||
|
||||
def contents(legacy: Path, root: Path | None = None) -> str:
|
||||
root = root or ROOT
|
||||
override, launcher = paths(legacy)
|
||||
def quote(path: Path) -> str:
|
||||
value = str(path)
|
||||
if any(c in value for c in '\n\r"\\$'):
|
||||
raise RuntimeError('Invalid BlueALSA service path')
|
||||
return '"' + value.replace('%', '%%') + '"'
|
||||
return ('[Service]\nExecStart=\nExecStart=/usr/bin/python3 ' +
|
||||
' '.join(quote(p) for p in (launcher, root, legacy, override)) + '\n')
|
||||
|
||||
|
||||
def check_owned(legacy: Path, root: Path | None = None) -> None:
|
||||
override, _ = paths(legacy)
|
||||
if override.exists() and override.read_text() != contents(legacy, root):
|
||||
raise RuntimeError('BlueALSA 5 override was changed externally')
|
||||
|
||||
|
||||
def install(legacy: Path, root: Path | None = None) -> None:
|
||||
root = root or ROOT
|
||||
validate(root)
|
||||
check_owned(legacy, root)
|
||||
override, launcher = paths(legacy)
|
||||
launcher.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = launcher.with_suffix('.tmp')
|
||||
temporary.write_bytes((root / 'launcher.py').read_bytes())
|
||||
temporary.chmod(0o644)
|
||||
temporary.replace(launcher)
|
||||
override.write_text(contents(legacy, root))
|
||||
|
||||
|
||||
def remove(legacy: Path, root: Path | None = None) -> None:
|
||||
check_owned(legacy, root)
|
||||
override, launcher = paths(legacy)
|
||||
override.unlink(missing_ok=True)
|
||||
launcher.unlink(missing_ok=True)
|
||||
@@ -16,7 +16,7 @@ import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.starpilot.soloist import backend
|
||||
from openpilot.starpilot.soloist import backend, bluealsa_runtime
|
||||
from openpilot.starpilot.soloist import preferences
|
||||
from openpilot.starpilot.soloist.paths import Layout
|
||||
from openpilot.starpilot.soloist.preferences import ADDRESS
|
||||
@@ -155,8 +155,8 @@ class PhoneInput:
|
||||
def capture_config(address: str) -> str:
|
||||
if not ADDRESS.fullmatch(address):
|
||||
raise ValueError('Invalid phone Bluetooth address')
|
||||
return ('</usr/share/alsa/alsa.conf>\npcm.starpilot_phone {\n type plug\n slave.pcm {\n' +
|
||||
f' type bluealsa\n device "{address}"\n profile "a2dp"\n' + ' }\n}\n')
|
||||
return ('</usr/share/alsa/alsa.conf>\n' + bluealsa_runtime.alsa_plugins() + 'pcm.starpilot_phone {\n type plug\n slave.pcm {\n' +
|
||||
f' type bluealsa\n device "{address}"\n profile "a2dp"\n hwcompat "silence"\n softvol "true"\n' + ' }\n}\n')
|
||||
|
||||
|
||||
class SampleSpec(C.Structure):
|
||||
@@ -246,7 +246,8 @@ def relay(address: str, server: str, config: Path):
|
||||
continue
|
||||
writer.write(data)
|
||||
if log is not None and now - last_log >= 0.5:
|
||||
log.write(f'{time.time():.3f} latency_ms={writer.latency_ms():.1f} max_read_ms={max_read * 1000:.0f}\n')
|
||||
epoch = time.time() # noqa: TID251 - correlate diagnostic logs with btmon/journal timestamps
|
||||
log.write(f'{epoch:.3f} latency_ms={writer.latency_ms():.1f} max_read_ms={max_read * 1000:.0f}\n')
|
||||
log.flush()
|
||||
last_log, max_read = now, 0.0
|
||||
finally:
|
||||
|
||||
@@ -11,6 +11,11 @@ from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
if __package__:
|
||||
from . import bluealsa_runtime
|
||||
else:
|
||||
import bluealsa_runtime
|
||||
|
||||
DROP_IN = Path('/run/systemd/system/bluealsa.service.d/90-starpilot-phone.conf')
|
||||
# BlueALSA is tiny but must drain the Bluetooth socket on time; at nice 0 it competes with driving
|
||||
# processes on busy cores and the music chain (nice 10) cannot make up for lost incoming packets.
|
||||
@@ -25,32 +30,40 @@ def systemctl(*args):
|
||||
def configure(action: str, path: Path = DROP_IN, run=systemctl) -> None:
|
||||
if action not in ('acquire', 'release'):
|
||||
raise ValueError('Unknown profile action')
|
||||
if path.exists() and path.read_text() not in (CONTENTS, LEGACY_CONTENTS):
|
||||
previous = path.read_text() if path.exists() else None
|
||||
if previous is not None and previous not in (CONTENTS, LEGACY_CONTENTS):
|
||||
raise RuntimeError('Phone audio profile override was changed externally')
|
||||
bluealsa_runtime.check_owned(path)
|
||||
override, _ = bluealsa_runtime.paths(path)
|
||||
if action == 'release':
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
if previous is not None or override.exists():
|
||||
bluealsa_runtime.remove(path)
|
||||
path.unlink(missing_ok=True)
|
||||
run('daemon-reload')
|
||||
run('try-restart', 'bluealsa.service')
|
||||
return
|
||||
if path.exists() and path.read_text() == LEGACY_CONTENTS:
|
||||
path.write_text(CONTENTS)
|
||||
run('daemon-reload')
|
||||
run('restart', 'bluealsa.service')
|
||||
return
|
||||
command = run('show', '--property=ExecStart', '--value', 'bluealsa.service')
|
||||
if 'argv[]=/usr/bin/bluealsa -p a2dp-source -p a2dp-sink ;' in command:
|
||||
bluealsa_runtime.validate()
|
||||
if previous == CONTENTS and override.exists():
|
||||
run('start', 'bluealsa.service')
|
||||
return
|
||||
if 'argv[]=/usr/bin/bluealsa -p a2dp-source ;' not in command:
|
||||
raise RuntimeError('Unsupported BlueALSA service configuration; phone audio was not enabled')
|
||||
if previous is None:
|
||||
command = run('show', '--property=ExecStart', '--value', 'bluealsa.service')
|
||||
if not any(f'argv[]=/usr/bin/bluealsa {profiles} ;' in command for profiles in
|
||||
('-p a2dp-source', '-p a2dp-source -p a2dp-sink')):
|
||||
raise RuntimeError('Unsupported BlueALSA service configuration; phone audio was not enabled')
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(CONTENTS)
|
||||
try:
|
||||
# Keep this file byte-for-byte compatible with old commits recovery code.
|
||||
path.write_text(CONTENTS)
|
||||
bluealsa_runtime.install(path)
|
||||
run('daemon-reload')
|
||||
run('restart', 'bluealsa.service')
|
||||
except Exception:
|
||||
path.unlink(missing_ok=True)
|
||||
bluealsa_runtime.remove(path)
|
||||
if previous is None:
|
||||
path.unlink(missing_ok=True)
|
||||
else:
|
||||
path.write_text(previous)
|
||||
run('daemon-reload')
|
||||
run('try-restart', 'bluealsa.service')
|
||||
raise
|
||||
|
||||
@@ -20,7 +20,7 @@ import time
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.starpilot.soloist import backend, bugsink_report, credentials, package, preferences, phone_audio
|
||||
from openpilot.starpilot.soloist import backend, bluealsa_runtime, bugsink_report, credentials, package, preferences, phone_audio
|
||||
from openpilot.starpilot.soloist import now_playing as now_playing_module
|
||||
from openpilot.starpilot.soloist.client import PlayerClient
|
||||
from openpilot.starpilot.soloist.paths import CONTROL_SOCKET, Layout, private_dir
|
||||
@@ -357,7 +357,7 @@ class Supervisor:
|
||||
if self.phone_prefs is not None:
|
||||
self.stop_children()
|
||||
self.retry_at = 0
|
||||
elif self.phone_profile and self.clock() >= self.pairing_profile_until:
|
||||
elif self.phone_profile and (prefs.output != 'bluetooth' or self.launched is None) and self.clock() >= self.pairing_profile_until:
|
||||
self._release_profile()
|
||||
state, message, launch_key = self.desired()
|
||||
now = self.clock()
|
||||
@@ -382,6 +382,14 @@ class Supervisor:
|
||||
if not self.network():
|
||||
self._set('waiting_network', 'Waiting for the comma’s network…')
|
||||
return
|
||||
if prefs.output == 'bluetooth' and not self.phone_profile:
|
||||
try:
|
||||
phone_audio.profile('acquire')
|
||||
self.phone_profile = True
|
||||
except (OSError, RuntimeError, subprocess.SubprocessError) as error:
|
||||
self.retry_at = now + OUTPUT_RETRY_SECONDS
|
||||
self._set('audio_unavailable', str(error))
|
||||
return
|
||||
self.output_available = prefs.output != 'bluetooth' or self._output_ready(prefs.output_address)
|
||||
self.output_check_at = now + OUTPUT_RETRY_SECONDS
|
||||
self.waiting_for = '' if self.output_available else prefs.output_name or 'your Bluetooth speaker'
|
||||
@@ -579,7 +587,8 @@ def main() -> None:
|
||||
os.nice(10) # music yields to everything driving-related; children inherit this
|
||||
supervisor = Supervisor()
|
||||
reap_orphans(supervisor.layout)
|
||||
if Path('/run/systemd/system/bluealsa.service.d/90-starpilot-phone.conf').exists():
|
||||
legacy_profile = Path('/run/systemd/system/bluealsa.service.d/90-starpilot-phone.conf')
|
||||
if legacy_profile.exists() or bluealsa_runtime.paths(legacy_profile)[0].exists():
|
||||
phone_audio.profile('release')
|
||||
stop = threading.Event()
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Temporary AGNOS BlueALSA receiving profile. Invoked as root with a fixed action.
|
||||
|
||||
Only a feature-owned /run drop-in is changed; Bluetooth power, bonds, bluetoothd,
|
||||
HFP and the on-disk service definition are untouched. Unknown service commands
|
||||
are rejected rather than replaced. The override disappears on reboot.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
DROP_IN = Path('/run/systemd/system/bluealsa.service.d/90-starpilot-phone.conf')
|
||||
# BlueALSA is tiny but must drain the Bluetooth socket on time; at nice 0 it competes with driving
|
||||
# processes on busy cores and the music chain (nice 10) cannot make up for lost incoming packets.
|
||||
LEGACY_CONTENTS = '[Service]\nExecStart=\nExecStart=/usr/bin/bluealsa -p a2dp-source -p a2dp-sink\n'
|
||||
CONTENTS = LEGACY_CONTENTS + 'Nice=-5\n'
|
||||
|
||||
|
||||
def systemctl(*args):
|
||||
return subprocess.run(['/usr/bin/systemctl', *args], check=True, capture_output=True, text=True, timeout=15).stdout.strip()
|
||||
|
||||
|
||||
def configure(action: str, path: Path = DROP_IN, run=systemctl) -> None:
|
||||
if action not in ('acquire', 'release'):
|
||||
raise ValueError('Unknown profile action')
|
||||
if path.exists() and path.read_text() not in (CONTENTS, LEGACY_CONTENTS):
|
||||
raise RuntimeError('Phone audio profile override was changed externally')
|
||||
if action == 'release':
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
run('daemon-reload')
|
||||
run('try-restart', 'bluealsa.service')
|
||||
return
|
||||
if path.exists() and path.read_text() == LEGACY_CONTENTS:
|
||||
path.write_text(CONTENTS)
|
||||
run('daemon-reload')
|
||||
run('restart', 'bluealsa.service')
|
||||
return
|
||||
command = run('show', '--property=ExecStart', '--value', 'bluealsa.service')
|
||||
if 'argv[]=/usr/bin/bluealsa -p a2dp-source -p a2dp-sink ;' in command:
|
||||
run('start', 'bluealsa.service')
|
||||
return
|
||||
if 'argv[]=/usr/bin/bluealsa -p a2dp-source ;' not in command:
|
||||
raise RuntimeError('Unsupported BlueALSA service configuration; phone audio was not enabled')
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(CONTENTS)
|
||||
try:
|
||||
run('daemon-reload')
|
||||
run('restart', 'bluealsa.service')
|
||||
except Exception:
|
||||
path.unlink(missing_ok=True)
|
||||
run('daemon-reload')
|
||||
run('try-restart', 'bluealsa.service')
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Serializes recovery and normal shutdown; never takes the adapter's pairing lease.
|
||||
with open('/run/starpilot-phone-profile.lock', 'w') as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||
configure(sys.argv[1])
|
||||
@@ -0,0 +1,145 @@
|
||||
from pathlib import Path
|
||||
import runpy
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from openpilot.starpilot.soloist import bluealsa_runtime as runtime, phone_profile, phone_audio, backend
|
||||
|
||||
|
||||
def test_bundle_is_complete_checksummed_arm64():
|
||||
runtime.validate()
|
||||
assert (runtime.ROOT / 'UPSTREAM_COMMIT').read_text().strip() == '1935d6dcb8975f2d7a51aaafe61538d157224623'
|
||||
|
||||
|
||||
def test_capture_and_playback_select_matching_private_plugins():
|
||||
capture = phone_audio.capture_config('11:22:33:44:55:66')
|
||||
playback = backend.alsa_config('11:22:33:44:55:66')
|
||||
for config in (capture, playback):
|
||||
assert runtime.alsa_plugins() in config
|
||||
assert 'softvol "true"' in config
|
||||
assert 'hwcompat "silence"' in capture
|
||||
assert 'hwcompat' not in playback
|
||||
|
||||
|
||||
def test_checksum_failure_rejects_incomplete_upgrade(tmp_path):
|
||||
shutil.copytree(runtime.ROOT, tmp_path / 'bundle', ignore=shutil.ignore_patterns('build', '__pycache__'))
|
||||
root = tmp_path / 'bundle'
|
||||
(root / 'lib/libasound_module_pcm_bluealsa.so').write_bytes(b'broken')
|
||||
with pytest.raises(RuntimeError, match='checksum'):
|
||||
runtime.validate(root)
|
||||
|
||||
|
||||
def test_old_commit_restart_restores_stock_daemon_and_removes_upgrade(tmp_path, monkeypatch):
|
||||
checkout = tmp_path / 'checkout'
|
||||
checkout.mkdir()
|
||||
def git(*args):
|
||||
return subprocess.run(['git', '-C', str(checkout), *args], check=True, capture_output=True, text=True).stdout.strip()
|
||||
git('init', '-q')
|
||||
git('config', 'user.name', 'Rollback test')
|
||||
git('config', 'user.email', 'rollback@example.invalid')
|
||||
old = Path(__file__).with_name('fixtures') / 'phone_profile_4_1_1.txt'
|
||||
shutil.copyfile(old, checkout / 'phone_profile.py')
|
||||
git('add', '.')
|
||||
git('commit', '-qm', '4.1.1')
|
||||
previous_commit = git('rev-parse', 'HEAD')
|
||||
bundle = checkout / 'bluealsa'
|
||||
shutil.copytree(runtime.ROOT, bundle, ignore=shutil.ignore_patterns('build', '__pycache__'))
|
||||
git('add', '.')
|
||||
git('commit', '-qm', '5.0.0')
|
||||
monkeypatch.setattr(runtime, 'ROOT', bundle)
|
||||
legacy = tmp_path / 'run/systemd/system/bluealsa.service.d/90-starpilot-phone.conf'
|
||||
calls = []
|
||||
def run(*args):
|
||||
calls.append(args)
|
||||
if args[0] == 'show':
|
||||
return '{ argv[]=/usr/bin/bluealsa -p a2dp-source ; }'
|
||||
return ''
|
||||
phone_profile.configure('acquire', legacy, run)
|
||||
override, launcher = runtime.paths(legacy)
|
||||
launch = runpy.run_path(str(launcher))['select']
|
||||
assert launch(bundle, legacy, override, lambda: None)[0] == str(bundle / 'bin/bluealsad')
|
||||
# Actual checkout removes tracked v5 bundle; /run intentionally survives.
|
||||
git('checkout', '-q', previous_commit)
|
||||
assert not (bundle / 'VERSION').exists()
|
||||
old_module = runpy.run_path(str(checkout / 'phone_profile.py'))
|
||||
selected = []
|
||||
def old_run(*args):
|
||||
calls.append(args)
|
||||
if args[0] == 'try-restart':
|
||||
selected.append(launch(bundle, legacy, override, lambda: calls.append(('daemon-reload',))))
|
||||
return ''
|
||||
old_module['configure']('release', legacy, old_run)
|
||||
assert selected == [['/usr/bin/bluealsa', '-p', 'a2dp-source']]
|
||||
assert not legacy.exists() and not override.exists() and not launcher.exists()
|
||||
old_module['configure']('acquire', legacy, run)
|
||||
assert legacy.read_text() == old_module['CONTENTS']
|
||||
assert not override.exists() # old code stays on the system daemon/plugins
|
||||
|
||||
|
||||
def test_failed_start_rolls_back_both_overrides(tmp_path):
|
||||
legacy = tmp_path / 'system/bluealsa.service.d/90-starpilot-phone.conf'
|
||||
calls = []
|
||||
def run(*args):
|
||||
calls.append(args)
|
||||
if args[0] == 'show':
|
||||
return '{ argv[]=/usr/bin/bluealsa -p a2dp-source ; }'
|
||||
if args[0] == 'restart':
|
||||
raise RuntimeError('start failed')
|
||||
return ''
|
||||
with pytest.raises(RuntimeError, match='start failed'):
|
||||
phone_profile.configure('acquire', legacy, run)
|
||||
assert not legacy.exists()
|
||||
assert all(not p.exists() for p in runtime.paths(legacy))
|
||||
assert calls[-1] == ('try-restart', 'bluealsa.service')
|
||||
|
||||
|
||||
def test_spotify_bluetooth_owns_upgrade_until_output_stops(tmp_path, monkeypatch):
|
||||
from openpilot.starpilot.soloist import preferences, supervisor
|
||||
from openpilot.starpilot.soloist.paths import Layout
|
||||
from openpilot.starpilot.soloist.tests.test_runtime import make_supervisor, ready_layout
|
||||
layout = Layout(tmp_path / 'music')
|
||||
ready_layout(layout, monkeypatch)
|
||||
preferences.save(preferences.Preferences(output='bluetooth', output_address='11:22:33:44:55:66'), layout)
|
||||
calls, children = [], []
|
||||
monkeypatch.setattr(phone_audio, 'profile', calls.append)
|
||||
monkeypatch.setattr(supervisor, '_kill_group', lambda process: None)
|
||||
sup, now = make_supervisor(layout, children)
|
||||
sup.output.ready = lambda address: True
|
||||
sup.tick()
|
||||
assert calls == ['acquire'] and sup.phone_profile and len(children) == 2
|
||||
now[0] += 300
|
||||
sup.tick()
|
||||
assert calls == ['acquire'] # pairing timeout must not downgrade a playing output
|
||||
sup.stop_children()
|
||||
assert calls == ['acquire', 'release'] and not sup.phone_profile
|
||||
|
||||
|
||||
def test_spotify_never_starts_v5_plugin_against_v4_after_activation_failure(tmp_path, monkeypatch):
|
||||
from openpilot.starpilot.soloist import preferences
|
||||
from openpilot.starpilot.soloist.paths import Layout
|
||||
from openpilot.starpilot.soloist.tests.test_runtime import make_supervisor, ready_layout
|
||||
layout = Layout(tmp_path / 'music')
|
||||
ready_layout(layout, monkeypatch)
|
||||
preferences.save(preferences.Preferences(output='bluetooth', output_address='11:22:33:44:55:66'), layout)
|
||||
def failed(action):
|
||||
raise RuntimeError('upgrade unavailable')
|
||||
monkeypatch.setattr(phone_audio, 'profile', failed)
|
||||
children = []
|
||||
sup, now = make_supervisor(layout, children)
|
||||
sup.tick()
|
||||
assert not children and not sup.phone_profile
|
||||
assert sup.state == 'audio_unavailable' and sup.retry_at > now[0]
|
||||
|
||||
|
||||
def test_daemon_uses_separate_persistent_storage():
|
||||
binary = (runtime.ROOT / 'bin/bluealsad').read_bytes()
|
||||
assert b'/data/starpilot/bluealsa5/lib/bluealsa\0' in binary
|
||||
assert b'/var/lib/bluealsa\0' not in binary
|
||||
|
||||
|
||||
def test_bundle_artifacts_are_not_git_ignored():
|
||||
result = subprocess.run(['git', '-C', str(runtime.ROOT.parents[3]), 'check-ignore',
|
||||
*(str(runtime.ROOT / name) for name in runtime.ARTIFACTS)], check=False, capture_output=True)
|
||||
assert result.returncode == 1 # all artifacts can travel with the upgrade commit
|
||||
@@ -168,6 +168,7 @@ class FakeClient:
|
||||
|
||||
|
||||
def ready_layout(layout, monkeypatch, *, expired=False):
|
||||
monkeypatch.setattr(supervisor.phone_audio, 'profile', lambda action: None)
|
||||
monkeypatch.setattr(supervisor.package, 'release_status', lambda _l: {'installed': True, 'expired': expired})
|
||||
monkeypatch.setattr(supervisor.backend, 'bundle_ready', lambda: True)
|
||||
layout.root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user