From 4e43e6d57bcb43252b3180151808a51991ece9c8 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sat, 8 Aug 2026 20:54:29 +0200 Subject: [PATCH] image parity: imsd 0.3.0 (pinned public commit) + callaudioshim + modem-daemon override imsd turns out to be fully public already (0.2.7 snapshot + emergency stage 1 + the make build path) - no tag needed, same git-archive dance as the kernel. callaudioshim was a load-bearing unpackaged home-dir script on the phone (callaudiod replacement, working in-call mic mute); now an aport autostarted at phase 1. kde-telephony's modem daemon autostart is hidden via /etc/skel from the imsd package, mirroring the phone's user override. Co-Authored-By: Claude Fable 5 --- README.md | 4 +- aports/device/callaudioshim/APKBUILD | 34 +++ .../callaudioshim/callaudioshim.desktop | 9 + aports/device/callaudioshim/callaudioshim.py | 195 ++++++++++++++++++ aports/modem/imsd/APKBUILD | 16 +- .../modem/imsd/org.kde.modem.daemon.desktop | 2 + build.sh | 23 ++- 7 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 aports/device/callaudioshim/APKBUILD create mode 100644 aports/device/callaudioshim/callaudioshim.desktop create mode 100644 aports/device/callaudioshim/callaudioshim.py create mode 100644 aports/modem/imsd/org.kde.modem.daemon.desktop diff --git a/README.md b/README.md index 555b1fb..543d95a 100644 --- a/README.md +++ b/README.md @@ -77,5 +77,7 @@ with the `aports/` overlay in this repository: - [x] Speaker-audio userspace files (`soc-fairphone-fp6-audio`: topology, amp config, UCM — byte-identical to the tested dev-phone set; blob provenance to be settled before this repo goes public) -- [ ] imsd 0.3.0 tag (aport currently needs it published) +- [x] imsd in the image (pinned to the public 0.3.0 commit; the phone's + working call stack incl. callaudioshim and the modem-daemon autostart + override ship as packages) - [ ] Nightly schedule once the first dispatched run is green diff --git a/aports/device/callaudioshim/APKBUILD b/aports/device/callaudioshim/APKBUILD new file mode 100644 index 0000000..61b7685 --- /dev/null +++ b/aports/device/callaudioshim/APKBUILD @@ -0,0 +1,34 @@ +# callaudiod replacement for the FP6. Stock callaudiod 0.1.99 never finds a +# voice-capable card on this hardware (UCM exposes HiFi with Speaker+Mic, no +# Earpiece port), leaving plasma-dialer's mute button dead and blocking +# kde-telephony-daemon 25s per call; see the script header for the full +# write-up. The callaudiod package stays installed (plasma-mobile depends on +# it) — this shim simply owns org.mobian_project.CallAudio first via an +# early-phase autostart, so D-Bus activation of the real daemon never fires. +# Deployed and tested on the development FP6. +maintainer="Jorijn van der Graaf " +pkgname=callaudioshim +pkgver=1 +pkgrel=0 +pkgdesc="callaudiod replacement shim: working in-call mic mute on the FP6" +url="https://forgejo.catcrafts.net/Catcrafts/fp6-img" +arch="noarch" +license="GPL-3.0-only" +depends="python3 py3-dbus py3-gobject3 wireplumber" +options="!check" +source=" + callaudioshim.py + callaudioshim.desktop +" + +package() { + install -Dm755 "$srcdir"/callaudioshim.py \ + "$pkgdir"/usr/libexec/callaudioshim.py + install -Dm644 "$srcdir"/callaudioshim.desktop \ + "$pkgdir"/etc/xdg/autostart/callaudioshim.desktop +} + +sha512sums=" +7ca7d75402ea6c48a7c1c19c5c5ed6e556f2c0133aa6e32a12f5c5a1afc0c791f04ebd40102a4b92b0dd87f9330ae64ada025c344525b9ed17289acd8cb952d7 callaudioshim.py +060b76660398115af26463425db5a6a2d634b6bdbf48a8ae488528403f8e086a9ef23470316196e6ccd8cb65e0ebd09bbc0df2ff5f8a830407d98404f439c706 callaudioshim.desktop +" diff --git a/aports/device/callaudioshim/callaudioshim.desktop b/aports/device/callaudioshim/callaudioshim.desktop new file mode 100644 index 0000000..0d347a0 --- /dev/null +++ b/aports/device/callaudioshim/callaudioshim.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=CallAudio Shim +Comment=callaudiod replacement: working mic mute for calls (org.mobian_project.CallAudio) +Exec=/usr/libexec/callaudioshim.py +Icon=audio-input-microphone +NoDisplay=true +X-KDE-autostart-phase=1 +X-KDE-StartupNotify=false diff --git a/aports/device/callaudioshim/callaudioshim.py b/aports/device/callaudioshim/callaudioshim.py new file mode 100644 index 0000000..b935be0 --- /dev/null +++ b/aports/device/callaudioshim/callaudioshim.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +"""callaudioshim.py — drop-in replacement for callaudiod on the FP6. + +Stock callaudiod 0.1.99 is broken on this hardware: it never finds a +"voice-capable" PulseAudio card (our UCM exposes HiFi with Speaker+Mic, no +Earpiece port), so + - its MicState property stays at the D-Bus default 0 = MIC_OFF and + plasma-dialer's in-call page permanently shows the mic as MUTED; + - MuteMic() bails with "card has no usable source" — the mute button does + nothing; + - SelectMode() never replies, blocking kde-telephony-daemon's main loop for + the full 25 s GDBus timeout on every call (journal/ims.md s41). + +A second, independent upstream defect (plasma-dialer 6.7.3) makes the UI state +wrong even with a working callaudiod: the mute button binds to +kde-telephony-daemon's DialerUtils `m_mute` — an UNINITIALIZED bool that +nothing ever syncs with reality (it only changes on button presses, and the +daemon never re-reads MicState). So every call page opened showing "muted" +while the mic was live, and the first press was wasted re-syncing. This shim +papers over that too: whenever the REAL mic state changes (and whenever +kde-telephony-daemon [re]appears on the bus), it pushes the truth into +DialerUtils.setMute(b), which updates m_mute and fires muteChanged to the UI. +The loop is self-terminating (the daemon only re-emits on value change, and +the bounce-back MuteMic is a no-op when the state already matches). + +This shim owns org.mobian_project.CallAudio instead and implements the same +interface (verified against callaudiod's org.mobian_project.CallAudio.xml): + - MuteMic(b) mutes/unmutes @DEFAULT_AUDIO_SOURCE@ via wpctl. The default + source is ec_source (the AEC'd mic, s43), which every capture stream — + including the call's pw-record — consumes, so mute = real uplink silence + (verified: tone capture RMS 94.5 -> 0.0 on mute). + - MicState mirrors the ACTUAL source mute (polled every 2 s too, so + out-of-band wpctl/pactl changes stay reflected), with PropertiesChanged + so kde-telephony-daemon's cached GDBusProxy tracks it. + - SelectMode / EnableSpeaker reply immediately (bookkeeping only: the + loudspeaker is the only output today, and UCM verb switching is not + dynamic in this setup — journal/audio.md 2026-07-18). SpeakerState + defaults to ON. When an Earpiece UCM device exists these become real. + +Stock callaudiod must not own the name: pkill it and mask its D-Bus +activation (see journal/ims.md s44 deploy notes). Runs as the session user +(autostart .desktop), no root needed. +""" +import subprocess + +import dbus +import dbus.service +import dbus.mainloop.glib +from gi.repository import GLib + +BUS_NAME = "org.mobian_project.CallAudio" +OBJ_PATH = "/org/mobian_project/CallAudio" +IFACE = "org.mobian_project.CallAudio" +PROPS_IFACE = "org.freedesktop.DBus.Properties" +SOURCE = "@DEFAULT_AUDIO_SOURCE@" + +# kde-telephony-daemon's DialerUtils — the mute-button state lives there +DIALER_BUS = "org.kde.telephony.DialerUtils" +DIALER_PATH = "/org/kde/telephony/DialerUtils/tel/mm" +DIALER_IFACE = "org.kde.telephony.DialerUtils" + +MODE_DEFAULT, MODE_CALL = 0, 1 +OFF, ON, UNKNOWN = 0, 1, 255 + + +def log(msg): + print(msg, flush=True) + + +def source_muted(): + """True/False from wpctl ('Volume: 1.00 [MUTED]'), None if unreadable.""" + try: + out = subprocess.run(["wpctl", "get-volume", SOURCE], + capture_output=True, text=True, timeout=5).stdout + except (OSError, subprocess.TimeoutExpired): + return None + if "Volume:" not in out: + return None + return "[MUTED]" in out + + +def set_source_muted(mute): + return subprocess.run( + ["wpctl", "set-mute", SOURCE, "1" if mute else "0"], + capture_output=True, timeout=5).returncode == 0 + + +class CallAudio(dbus.service.Object): + def __init__(self, bus): + super().__init__(bus, OBJ_PATH) + self.bus = bus + self.audio_mode = MODE_DEFAULT + self.speaker_state = ON # loudspeaker is the only output today + m = source_muted() + self.mic_state = UNKNOWN if m is None else (OFF if m else ON) + log(f"callaudioshim: up, MicState={self.mic_state}") + GLib.timeout_add_seconds(2, self._poll) + self._push_dialer() + # re-push whenever kde-telephony-daemon (re)appears: its m_mute is an + # uninitialized bool until someone calls setMute + bus.watch_name_owner(DIALER_BUS, self._dialer_owner_changed) + + # -------------------------------------------- DialerUtils state push + def _dialer_owner_changed(self, owner): + if owner: + log(f"callaudioshim: DialerUtils owner {owner}; pushing mute state") + self._push_dialer() + + def _push_dialer(self): + if self.mic_state == UNKNOWN: + return + try: + self.bus.call_async(DIALER_BUS, DIALER_PATH, DIALER_IFACE, + "setMute", "b", (self.mic_state == OFF,), + lambda *a: None, lambda *a: None) + except dbus.DBusException: + pass + + # ------------------------------------------------ property plumbing + def _props(self): + return { + "AudioMode": dbus.UInt32(self.audio_mode), + "SpeakerState": dbus.UInt32(self.speaker_state), + "MicState": dbus.UInt32(self.mic_state), + } + + def _set(self, attr, prop, value): + if getattr(self, attr) == value: + return + setattr(self, attr, value) + self.PropertiesChanged(IFACE, {prop: dbus.UInt32(value)}, []) + if prop == "MicState": + self._push_dialer() + + def _poll(self): + # catch out-of-band mute changes (wpctl, pactl, other clients) + m = source_muted() + if m is not None: + self._set("mic_state", "MicState", OFF if m else ON) + return True + + @dbus.service.method(PROPS_IFACE, in_signature="ss", out_signature="v") + def Get(self, interface, prop): + return self._props()[prop] + + @dbus.service.method(PROPS_IFACE, in_signature="s", out_signature="a{sv}") + def GetAll(self, interface): + return self._props() + + @dbus.service.signal(PROPS_IFACE, signature="sa{sv}as") + def PropertiesChanged(self, interface, changed, invalidated): + pass + + # ------------------------------------------------ org.mobian_project.CallAudio + @dbus.service.method(IFACE, in_signature="u", out_signature="b") + def SelectMode(self, mode): + if mode not in (MODE_DEFAULT, MODE_CALL): + raise dbus.exceptions.DBusException( + "invalid mode", name="org.freedesktop.DBus.Error.InvalidArgs") + log(f"callaudioshim: SelectMode({int(mode)})") + self._set("audio_mode", "AudioMode", int(mode)) + if mode == MODE_DEFAULT: + # leaving a call: never leave the mic muted behind + if source_muted(): + set_source_muted(False) + self._set("mic_state", "MicState", ON) + return True + + @dbus.service.method(IFACE, in_signature="b", out_signature="b") + def EnableSpeaker(self, enable): + log(f"callaudioshim: EnableSpeaker({bool(enable)})") + self._set("speaker_state", "SpeakerState", ON if enable else OFF) + return True + + @dbus.service.method(IFACE, in_signature="b", out_signature="b") + def MuteMic(self, mute): + ok = set_source_muted(bool(mute)) + log(f"callaudioshim: MuteMic({bool(mute)}) -> {ok}") + if not ok: + return False + self._set("mic_state", "MicState", OFF if mute else ON) + return True + + +def main(): + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + session = dbus.SessionBus() + name = dbus.service.BusName(BUS_NAME, session, do_not_queue=True) # noqa: F841 + CallAudio(session) + log(f"callaudioshim: owning {BUS_NAME}") + GLib.MainLoop().run() + + +if __name__ == "__main__": + main() diff --git a/aports/modem/imsd/APKBUILD b/aports/modem/imsd/APKBUILD index 13ce0ca..1a9238b 100644 --- a/aports/modem/imsd/APKBUILD +++ b/aports/modem/imsd/APKBUILD @@ -22,7 +22,16 @@ provides="81voltd=$pkgver-r$pkgrel" # no OpenRC service yet: the unit's PDN-bring-up/env-file sequencing is only # tested under systemd; an initd is welcome once someone can verify one subpackages="$pkgname-systemd" -source="$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz" +# Pinned by CI (see build.sh at the repository root): the Forgejo instance +# serves no source archives, so the tarball is generated with git-archive +# (prefix imsd/) and placed next to this APKBUILD. The skel override hides +# kde-telephony's modem daemon autostart for the account created at install — +# imsd-dialerd owns those session D-Bus names instead. +_commit="REPLACED_BY_CI" +source=" + imsd-$_commit.tar.gz + org.kde.modem.daemon.desktop +" builddir="$srcdir/$pkgname" build() { @@ -35,6 +44,8 @@ check() { package() { make install DESTDIR="$pkgdir" + install -Dm644 "$srcdir"/org.kde.modem.daemon.desktop \ + "$pkgdir"/etc/skel/.config/autostart/org.kde.modem.daemon.desktop } systemd() { @@ -44,5 +55,6 @@ systemd() { } sha512sums=" -PLACEHOLDER_RUN_PMBOOTSTRAP_CHECKSUM_AFTER_V0_3_0_TAG_EXISTS imsd-0.3.0.tar.gz +REPLACED_BY_CI imsd-REPLACED_BY_CI.tar.gz +REPLACED_BY_CI org.kde.modem.daemon.desktop " diff --git a/aports/modem/imsd/org.kde.modem.daemon.desktop b/aports/modem/imsd/org.kde.modem.daemon.desktop new file mode 100644 index 0000000..e1e3e17 --- /dev/null +++ b/aports/modem/imsd/org.kde.modem.daemon.desktop @@ -0,0 +1,2 @@ +[Desktop Entry] +Hidden=true diff --git a/build.sh b/build.sh index c47e171..a4a9461 100755 --- a/build.sh +++ b/build.sh @@ -17,6 +17,10 @@ set -eu KERNEL_REPO=https://forgejo.catcrafts.net/Catcrafts/milos-linux.git KERNEL_BRANCH=combined-stable +IMSD_REPO=https://forgejo.catcrafts.net/Catcrafts/imsd.git +# imsd 0.3.0: what the dev phone runs (0.2.7) + emergency-calling stage 1 + +# the make build path the aport needs. Bump deliberately, not via tip-chasing. +IMSD_COMMIT=1037958 PMAPORTS_REPO=https://gitlab.postmarketos.org/postmarketOS/pmaports.git cd "$(dirname "$0")" @@ -76,6 +80,7 @@ rm -rf "$WORK/pmaports/device/testing/linux-postmarketos-qcom-milos" \ "$WORK/pmaports/main/postmarketos-config-nftables" cp -r aports/device/linux-postmarketos-qcom-milos "$WORK/pmaports/device/testing/" cp -r aports/device/soc-fairphone-fp6-audio "$WORK/pmaports/device/" +cp -r aports/device/callaudioshim "$WORK/pmaports/device/" cp -r aports/main/postmarketos-config-nftables "$WORK/pmaports/main/" cp -r aports/modem/imsd "$WORK/pmaports/modem/" @@ -90,13 +95,19 @@ git -C "$WORK/milos-src" archive --prefix=milos-linux/ \ -o "$KAPORT/milos-linux-$COMMIT.tar.gz" HEAD sed -i "s/^_commit=.*/_commit=\"$COMMIT\"/" "$KAPORT/APKBUILD" +# Same dance for imsd, pinned to a reviewed commit rather than branch tip. +rm -rf "$WORK/imsd-src" +git clone -q "$IMSD_REPO" "$WORK/imsd-src" +git -C "$WORK/imsd-src" checkout -q "$IMSD_COMMIT" +IAPORT="$WORK/pmaports/modem/imsd" +git -C "$WORK/imsd-src" archive --prefix=imsd/ \ + -o "$IAPORT/imsd-$IMSD_COMMIT.tar.gz" HEAD +sed -i "s/^_commit=.*/_commit=\"$IMSD_COMMIT\"/" "$IAPORT/APKBUILD" + # --- 3. configure pmbootstrap ------------------------------------------------- # 'pmbootstrap config' refuses to run before a config exists ("run init # first"), so write the config file directly (INI, [pmbootstrap] section, # keys = pmb.core.Config attributes). -# Stage 1 builds WITHOUT imsd: its aport sources the v0.3.0 tag, which is not -# published yet. When it is: add "extra_packages = imsd" and enable the -# checksum line below. # 'init' would also create the work dir and stamp its migration version; # do both ourselves (version derived from the installed pmb, not hardcoded). WORKDIR="$HOME/.local/var/pmbootstrap" @@ -110,11 +121,11 @@ aports = $WORK/pmaports device = fairphone-fp6 ui = plasma-mobile systemd = always -extra_packages = soc-fairphone-fp6-audio +extra_packages = soc-fairphone-fp6-audio,callaudioshim,imsd EOF pmbootstrap checksum linux-postmarketos-qcom-milos -#pmbootstrap checksum imsd +pmbootstrap checksum imsd # --- 4. build the image ------------------------------------------------------- # Same default credentials as the official postmarketOS images. @@ -132,7 +143,7 @@ cp -L "$EXPORT/fairphone-fp6.img" dist/fairphone-fp6.img echo "kernel: $KERNEL_REPO $KERNEL_BRANCH @ $COMMIT" echo "built: $(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "default login: user / 147147 (same as official postmarketOS images)" - echo "imsd: not included yet (waiting on the v0.3.0 tag)" + echo "imsd: $IMSD_REPO @ $IMSD_COMMIT (0.3.0)" } > dist/build-info.txt cp README.md dist/README.md