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 <noreply@anthropic.com>
This commit is contained in:
parent
719a10a997
commit
4e43e6d57b
7 changed files with 274 additions and 9 deletions
195
aports/device/callaudioshim/callaudioshim.py
Normal file
195
aports/device/callaudioshim/callaudioshim.py
Normal file
|
|
@ -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()
|
||||
Loading…
Reference in a new issue