diff --git a/Makefile b/Makefile index 758d1ba..1428dec 100644 --- a/Makefile +++ b/Makefile @@ -7,11 +7,10 @@ # flags. Keep the two in sync when products, module partitions or link # flags change. # -# make -> build/make/{imsd,imsd-media,imsd-dialerd,imsd-ofonod} +# make -> build/make/{imsd,imsd-media,imsd-dialerd} # make check -> build + run the 7 unit-test suites # make install -> DESTDIR/PREFIX staged install (binaries + systemd -# units, D-Bus policies, dialerd autostart, the GNOME -# Calls plugin override, ims-pdn-up.sh) +# unit, D-Bus policy, dialerd autostart, ims-pdn-up.sh) # # Requires clang++ with libc++ and the libc++ std module sources # (std.cppm — Alpine: llvm-runtimes, Arch: libc++). Override STD_CPPM if @@ -55,7 +54,7 @@ CORE_OBJS = $(CORE_MODULES:%=$(OBJ)/%.o) TESTS = Util Aka Ipsec Messages Engine Sip Sdp -all: $(O)/imsd $(O)/imsd-media $(O)/imsd-dialerd $(O)/imsd-ofonod +all: $(O)/imsd $(O)/imsd-media $(O)/imsd-dialerd # ---- module BMIs ---------------------------------------------------------- @@ -104,13 +103,6 @@ $(OBJ)/dialerd.o: implementations/dialerd.cpp $(PCM)/std.pcm | $(OBJ) $(O)/imsd-dialerd: $(OBJ)/dialerd.o $(CXX) $(ALL_CXXFLAGS) $(ALL_LDFLAGS) $^ $(GIO_LIBS) -o $@ -$(OBJ)/ofonod.o: implementations/ofonod.cpp $(PCM)/std.pcm | $(OBJ) - $(CXX) $(ALL_CXXFLAGS) $(GIO_CFLAGS) -fprebuilt-module-path=$(PCM) \ - -c $< -o $@ - -$(O)/imsd-ofonod: $(OBJ)/ofonod.o - $(CXX) $(ALL_CXXFLAGS) $(ALL_LDFLAGS) $^ $(GIO_LIBS) -o $@ - # ---- tests ----------------------------------------------------------------- $(O)/tests/%: tests/%/main.cpp $(PCM)/Imsd.pcm $(O)/libimsd-core.a | $(O)/tests @@ -129,7 +121,6 @@ PREFIX ?= /usr install: all install -Dm755 $(O)/imsd $(DESTDIR)$(PREFIX)/bin/imsd install -Dm755 $(O)/imsd-dialerd $(DESTDIR)$(PREFIX)/bin/imsd-dialerd - install -Dm755 $(O)/imsd-ofonod $(DESTDIR)$(PREFIX)/bin/imsd-ofonod install -Dm755 $(O)/imsd-media $(DESTDIR)$(PREFIX)/libexec/imsd-media install -Dm755 packaging/ims-pdn-up.sh \ $(DESTDIR)$(PREFIX)/libexec/ims-pdn-up.sh @@ -139,12 +130,6 @@ install: all $(DESTDIR)/etc/xdg/autostart/imsd-dialerd.desktop install -Dm644 packaging/net.catcrafts.IMS1.conf \ $(DESTDIR)$(PREFIX)/share/dbus-1/system.d/net.catcrafts.IMS1.conf - install -Dm644 packaging/imsd-ofonod.service \ - $(DESTDIR)$(PREFIX)/lib/systemd/system/imsd-ofonod.service - install -Dm644 packaging/org.ofono.conf \ - $(DESTDIR)$(PREFIX)/share/dbus-1/system.d/imsd-ofono.conf - install -Dm644 packaging/90_imsd-ofono.gschema.override \ - $(DESTDIR)$(PREFIX)/share/glib-2.0/schemas/90_imsd-ofono.gschema.override $(PCM) $(OBJ) $(O)/tests: mkdir -p $@ diff --git a/README.md b/README.md index e2b3fdf..17c2c94 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,8 @@ Modem firmware on typical mainline-Linux phones will bring up the IMS PDN but can't be used for actual call audio. imsd implements the whole IMS client in userspace instead: SIP registration with USIM AKA authentication over kernel IPsec (ESP), call signalling, and the AMR-WB RTP media plane wired into -PipeWire so a stock mobile dialer — Plasma Dialer, or GNOME Calls under Phosh -and GNOME Mobile — can place and receive real VoLTE calls on a commercial -network. +PipeWire so a stock Plasma Mobile dialer can place and receive real VoLTE +calls on a commercial network. ## Status @@ -29,7 +28,7 @@ live only in the `imsd` daemon shell. ## Architecture -C++26 modules, built with Crafter Build. Five build products: +C++26 modules, built with Crafter Build. Four build products: - **imsd-core** (static library) — the engine, pure `import std` C++ with no GLib and no I/O side effects. Everything here is unit-testable on any dev @@ -49,26 +48,6 @@ C++26 modules, built with Crafter Build. Five build products: that the root system daemon cannot own; built with `-- --product=dialerd`, autostarted from an `.desktop` file in place of plasma-dialer's `modem-daemon` (whose autostart must be disabled). -- **imsd-ofonod** (executable) — the GNOME Calls integration, for Phosh and - GNOME Mobile: a system daemon owning `org.ofono` and presenting imsd as an - oFono modem, which is the seam GNOME Calls' bundled `ofono` provider plugin - drives. GDBus translation only, no core; built with `-- --product=ofonod`. - See [GNOME Calls (Phosh, GNOME Mobile)](#gnome-calls-phosh-gnome-mobile). - -### Desktop integration - -imsd itself is desktop-agnostic: everything a dialer needs is on the system -bus as `net.catcrafts.IMS1`. Each desktop gets a small translation daemon -speaking whatever backend protocol its dialer already consumes, so the dialer, -the shell and the call-history/contacts stack all stay unmodified. - -| Desktop | Daemon | Owns | Consumed by | -|---|---|---|---| -| Plasma Mobile | `imsd-dialerd` | `org.kde.telephony.*` (session bus) | plasma-dialer + kde-telephony-daemon, replacing `modem-daemon` | -| Phosh / GNOME Mobile | `imsd-ofonod` | `org.ofono` (system bus) | GNOME Calls' `ofono` provider plugin | - -Phosh's own call UI needs nothing extra: it consumes `org.gnome.Calls`, which -GNOME Calls exports once it has a working origin. ### Terminating-network dependency (incoming) @@ -106,8 +85,7 @@ gio-2.0 headers (`glib2` on Arch, `glib-dev` on Alpine). ```sh crafter-build # bin/imsd--/imsd (the daemon) crafter-build -- --product=media # bin/imsd-media-.../imsd-media (the media leg) -crafter-build -- --product=dialerd # bin/imsd-dialerd-.../imsd-dialerd (Plasma backend) -crafter-build -- --product=ofonod # bin/imsd-ofonod-.../imsd-ofonod (GNOME Calls backend) +crafter-build -- --product=dialerd # bin/imsd-dialerd-.../imsd-dialerd (dialer backend) crafter-build test # unit tests (Util, Aka, Ipsec, Messages, Engine, Sip, Sdp) ``` @@ -119,7 +97,7 @@ sources (`std.cppm` — package `llvm-runtimes` on Alpine, `libc++` on Arch), lld, and gio-2.0 headers. ```sh -make # build/make/{imsd,imsd-media,imsd-dialerd,imsd-ofonod} +make # build/make/{imsd,imsd-media,imsd-dialerd} make check # the same 7 unit-test suites make install # DESTDIR/PREFIX staged install incl. the packaging/ files ``` @@ -156,86 +134,6 @@ the granted lifetime. `packaging/APKBUILD` builds the apk from source via the Makefile; `packaging/APKBUILD.binary` + `make-bin-tarball.sh` wrap a cross-compiled build into an apk instead. -## GNOME Calls (Phosh, GNOME Mobile) - -`imsd-ofonod` presents imsd on the system bus as an oFono modem — a Manager at -`/`, one Modem + VoiceCallManager at `/imsd`, and a VoiceCall object per live -call — which is what GNOME Calls' bundled `ofono` provider plugin drives. -Calls, Phosh and GNOME Shell run unmodified. - -`org.ofono` is the seam because it is the only one a third party can -implement. `org.gnome.Calls.Call` is what Calls *exports* so a shell can -observe and control a call that already exists (Accept, Hangup, SendDtmf, -Silence — there is no Dial); Phosh consumes it from Calls. Calls' own backends -are libpeas provider plugins (`mm`, `ofono`, `sip`, `dummy`) linked against -private headers that no distro installs. - -Setup: - -```sh -apk add imsd-ofono # or install the files by hand -systemctl enable --now imsd-ofonod # system service; org.ofono is a system name -# point Calls at the ofono provider (the package ships this as a gschema -# override; this is the per-user equivalent) -gsettings set org.gnome.Calls autoload-plugins "['ofono']" -``` - -Drop `mm` from `autoload-plugins` rather than adding `ofono` alongside it: the -ModemManager provider would offer the modem's own CS voice path as a second -origin, and on a phone whose voice path is imsd that origin carries no audio. - -Do not run this alongside a real `ofonod` — there is one `org.ofono` name and -one owner; the unit declares `Conflicts=ofono.service` and the daemon exits if -it loses the name. Running `imsd-dialerd` at the same time is fine (different -bus, different consumer). - -Verified on a Fairphone 6 (postmarketOS, dbus-broker) with GNOME Calls 50.0: -outgoing and incoming calls with audio both ways, answered and hung up through -`org.gnome.Calls.Call` — the surface Phosh drives. One dbus-broker detail is -load-bearing and lives in the policy file: dbus-broker checks a broadcast -against the sender's send policy for every name the receiver owns, so root -(imsd) must be allowed `send_destination="org.ofono"` or imsd's call signals -never reach the daemon. - -Known gaps, all inherited rather than introduced: - -- **USSD is refused** with `org.ofono.Error.NotSupported`; the IMS stack has no - USSD path. It is deliberately not advertised in the modem's `Interfaces`. -- **No hold, multiparty, transfer or call waiting** — imsd is a single-call - stack, so those methods return `org.ofono.Error.NotImplemented`. -- **Per-call caller-ID withholding is refused** rather than silently ignored: - imsd cannot honour a CLIR request, and failing is safer than dialling with - the caller ID exposed. -- **Phosh's emergency-call screen lists nothing.** Not for want of the data: - `imsd-ofonod` publishes `org.ofono.VoiceCallManager.EmergencyNumbers` (112, - 911, plus `EMERGENCY_NUMBERS`), which is oFono's standard place for it. - Calls' ofono provider simply never reads it — it makes no `GetProperties` - call on the VoiceCallManager, and returns NULL for the origin's - `emergency-numbers`, which is what - `org.gnome.Calls.EmergencyCalls.GetEmergencyContacts` is built from. (The - ModemManager provider does read it, via `mm_sim_dup_emergency_numbers()`.) - An upstream fix would close this. Dialling an emergency number from the - normal dialpad is unaffected — that reaches imsd's own classifier. -- DTMF is forwarded to imsd, where `SendDtmf` is still a stub (RFC 4733 is not - implemented yet) — the same limitation Plasma has. -- **Restart GNOME Calls after restarting `imsd-ofonod` (or imsd).** When the - modem's `VoiceCallManager` interface goes away and comes back, Calls 50.0 - adds a new origin but leaves the old origin's `CallAdded` handler connected: - after one such cycle every call is added twice, after a few the stale handler - runs on freed memory and Calls dies with SIGBUS in `g_hash_table_lookup` - (measured on the FP6). Calls-side; an upstream fix belongs in - `plugins/provider/ofono/calls-ofono-provider.c`. - -Development, without a phone or root: - -```sh -./bin/imsd-*/imsd --session & # imsd's ABI on the session bus -./bin/imsd-ofonod-*/imsd-ofonod --session -busctl --user call org.ofono / org.ofono.Manager GetModems -# or keep org.ofono on the system bus and imsd on the session bus: -IMSD_BUS=session imsd-ofonod -``` - ## Configuration Everything is environment variables. The packaged unit reads @@ -306,9 +204,6 @@ integrations depend on it: | `CallDeleted` | `s` | | `RegistrationChanged` | `b` | -Both `imsd-dialerd` and `imsd-ofonod` are pure translations of this ABI; a new -desktop needs a new translation daemon, not changes here. - ## License GPL-3.0-only — see [LICENSE](LICENSE). @@ -317,3 +212,4 @@ GPL-3.0-only — see [LICENSE](LICENSE). Copyright (C) 2026 Catcrafts® catcrafts.net +| `CODECS` | *(empty — defaults)* | comma-separated codec preference list over `AMR-WB`, `AMR` (or `AMR-NB`), `PCMA`, `PCMU`: restricts and orders both the codecs offered on an outgoing call and those accepted from an inbound offer (default: offer AMR-WB + AMR, accept all four in that order). A bench knob — a network whose gateway transcodes every caller up to AMR-WB otherwise never lets the narrowband path run | diff --git a/implementations/main.cpp b/implementations/main.cpp index f297f42..6402382 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -41,7 +41,7 @@ import Imsd; namespace { // ---- static config (env-overridable, same knobs as imsd.py) --------------- -constexpr const char* Version = "0.3.7"; +constexpr const char* Version = "0.3.6"; constexpr const char* BusName = "net.catcrafts.IMS1"; constexpr const char* ObjPath = "/net/catcrafts/IMS1"; constexpr const char* Iface = "net.catcrafts.IMS1"; diff --git a/implementations/ofonod.cpp b/implementations/ofonod.cpp deleted file mode 100644 index 79c941e..0000000 --- a/implementations/ofonod.cpp +++ /dev/null @@ -1,770 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// lint-disable-file fixed-width-types no-char-pointer -/* -imsd-ofonod — GNOME Calls backend for imsd (Phosh, GNOME Mobile). - -Presents imsd as an oFono modem on the system bus: `org.ofono` with a Manager -at `/`, one Modem + VoiceCallManager at `/imsd`, and one VoiceCall object per -live call. GNOME Calls' bundled `ofono` provider plugin then drives imsd — -dial, ring, answer, hang up, call history, contact lookup — with Calls, Phosh -and GNOME Shell unmodified. It is the exact mirror of what imsd-dialerd does -for Plasma Mobile's org.kde.telephony.*. - -Why this seam and not org.gnome.Calls: that interface is what Calls *exports* -so a shell can observe and control a call that already exists (Accept, Hangup, -SendDtmf, Silence — there is no Dial, and no way to register as a call -source). Phosh consumes it from Calls; nobody provides it to Calls. Calls' -own backends are libpeas provider plugins (mm, ofono, sip, dummy) linked -against private headers that no distro installs, so `org.ofono` is the only -D-Bus seam a third party can implement. - -Consumer-shaped details, verified against calls 50.0 -plugins/provider/ofono/ — get these wrong and the plugin silently ignores us: - - the provider watches `org.ofono` on the SYSTEM bus and proxies `/` as - org.ofono.Manager. A modem becomes a dialable Origin only while its - `Interfaces` property contains "org.ofono.VoiceCallManager", so that list - is our registration gate — the analogue of the dialerd's deviceUniList. - - VoiceCall `State` strings are parsed as CallsCallState enum nicks: - dialing / alerting / incoming / active / held / disconnected. imsd's - "ringing" (a 180 from the far end) is oFono's "alerting". - - inbound-ness is derived ONCE, from State == "incoming" as the call is - added, so an MT call must be announced incoming and never corrected later. - - DisconnectReason is cached by the plugin and read when CallRemoved - arrives — emit it before the removal or the history entry loses its - reason. - - Dial's returned object path is ignored (the plugin waits for CallAdded). - We return a real one anyway. - -Runs as root because `org.ofono` is a system-bus name (policy in -packaging/org.ofono.conf). It does no I/O beyond D-Bus: every call decision -stays in imsd. Everything happens on the GLib main loop, so there is no -locking. - - --session own org.ofono on the session bus and talk to imsd there - (development: no root, no policy file) - IMSD_BUS=session|system override only the imsd side of that choice -*/ - -#include -#include - -import std; - -namespace { - -constexpr const char* ImsdBus = "net.catcrafts.IMS1"; -constexpr const char* ImsdPath = "/net/catcrafts/IMS1"; -constexpr const char* ImsdIface = "net.catcrafts.IMS1"; - -constexpr const char* OfonoBus = "org.ofono"; -constexpr const char* ManagerPath = "/"; -constexpr const char* ManagerIface = "org.ofono.Manager"; -constexpr const char* ModemPath = "/imsd"; -constexpr const char* ModemIface = "org.ofono.Modem"; -constexpr const char* VcmIface = "org.ofono.VoiceCallManager"; -constexpr const char* CallIface = "org.ofono.VoiceCall"; -constexpr const char* SsIface = "org.ofono.SupplementaryServices"; - -constexpr const char* ErrFailed = "org.ofono.Error.Failed"; -constexpr const char* ErrNotImplemented = "org.ofono.Error.NotImplemented"; -constexpr const char* ErrNotSupported = "org.ofono.Error.NotSupported"; -constexpr const char* ErrInvalidArgs = "org.ofono.Error.InvalidArguments"; - -void Log(std::string_view m) { std::println("imsd-ofonod: {}", m); std::fflush(stdout); } - -std::string EnvOr(const char* k, std::string d) { - const char* v = std::getenv(k); - return v ? std::string(v) : std::move(d); -} - -// ---- state (main thread only) ---------------------------------------------- -GDBusConnection* Bus = nullptr; // where we own org.ofono -GDBusConnection* Imsd = nullptr; // where net.catcrafts.IMS1 lives -GMainLoop* Loop = nullptr; -bool ImsdUp = false; -bool Registered = false; -std::vector EmergencyNumbers; - -struct CallRec { - std::string uni, path, number; - std::string state = "dialing"; // oFono nick, already mapped - std::string reason; // oFono DisconnectReason, once known - bool inbound = false; - std::int64_t startedAt = 0; - guint regId = 0; // exported VoiceCall object -}; - -std::map Calls; // uni -> record -std::map ByPath; // object path -> uni -std::map UniToPath; // uni -> object path (survives - // a Dial reply that races the - // CallAdded signal) -unsigned PathSeq = 0; - -// ---- GVariant helpers ------------------------------------------------------- -std::string DictStr(GVariant* dict, const char* key) { - GVariant* v = g_variant_lookup_value(dict, key, G_VARIANT_TYPE_STRING); - if (!v) return ""; - std::string s = g_variant_get_string(v, nullptr); - g_variant_unref(v); - return s; -} -std::int64_t DictInt64(GVariant* dict, const char* key) { - GVariant* v = g_variant_lookup_value(dict, key, G_VARIANT_TYPE_INT64); - if (!v) return 0; - std::int64_t x = g_variant_get_int64(v); - g_variant_unref(v); - return x; -} -bool DictBool(GVariant* dict, const char* key) { - GVariant* v = g_variant_lookup_value(dict, key, G_VARIANT_TYPE_BOOLEAN); - if (!v) return false; - bool b = g_variant_get_boolean(v); - g_variant_unref(v); - return b; -} - -// ---- imsd <-> oFono vocabulary --------------------------------------------- -// imsd call states -> oFono VoiceCall State nicks. Returns nullptr for a state -// we don't know, so the caller keeps the one it already published rather than -// inventing a transition. -const char* OfonoState(std::string_view s) { - if (s == "dialing") return "dialing"; - if (s == "ringing") return "alerting"; // 180 from the far end - if (s == "incoming") return "incoming"; - if (s == "active") return "active"; - if (s == "terminated") return "disconnected"; - return nullptr; -} - -// imsd hangup reasons -> the three oFono DisconnectReason values. -const char* OfonoReason(std::string_view r) { - if (r == "local-hangup") return "local"; - if (r == "remote-hangup" || r == "refused-or-busy") return "remote"; - return "network"; -} - -// ---- signal emission -------------------------------------------------------- -void Emit(const char* path, const char* iface, const char* name, GVariant* params) { - if (!Bus) return; - g_dbus_connection_emit_signal(Bus, nullptr, path, iface, name, params, nullptr); -} - -GVariant* PropChanged(const char* name, GVariant* value) { - return g_variant_new("(sv)", name, value); -} - -// ---- Modem / VoiceCallManager properties ------------------------------------ -bool VoiceUp() { return ImsdUp && Registered; } - -// The registration gate: the plugin only builds an Origin while -// org.ofono.VoiceCallManager is listed here. -GVariant* InterfacesValue() { - GVariantBuilder b; - g_variant_builder_init(&b, G_VARIANT_TYPE("as")); - if (VoiceUp()) g_variant_builder_add(&b, "s", VcmIface); - return g_variant_builder_end(&b); -} - -GVariant* ModemProps() { - GVariantBuilder b; - g_variant_builder_init(&b, G_VARIANT_TYPE("a{sv}")); - g_variant_builder_add(&b, "{sv}", "Name", g_variant_new_string("imsd")); - g_variant_builder_add(&b, "{sv}", "Manufacturer", g_variant_new_string("Catcrafts")); - g_variant_builder_add(&b, "{sv}", "Model", g_variant_new_string("userspace IMS")); - g_variant_builder_add(&b, "{sv}", "Type", g_variant_new_string("hardware")); - g_variant_builder_add(&b, "{sv}", "Powered", g_variant_new_boolean(TRUE)); - g_variant_builder_add(&b, "{sv}", "Online", g_variant_new_boolean(VoiceUp() ? TRUE : FALSE)); - g_variant_builder_add(&b, "{sv}", "Lockdown", g_variant_new_boolean(FALSE)); - g_variant_builder_add(&b, "{sv}", "Interfaces", InterfacesValue()); - return g_variant_builder_end(&b); -} - -// EmergencyNumbers mirrors imsd's own configuration (builtin 112/911 plus the -// EMERGENCY_NUMBERS list) so an oFono client can display it. Classification -// itself is imsd's — it decides at Dial time whether a number becomes an -// urn:service:sos INVITE — and this list never gates a dial here. -GVariant* VcmProps() { - GVariantBuilder b; - g_variant_builder_init(&b, G_VARIANT_TYPE("a{sv}")); - GVariantBuilder n; - g_variant_builder_init(&n, G_VARIANT_TYPE("as")); - for (const std::string& e : EmergencyNumbers) - g_variant_builder_add(&n, "s", e.c_str()); - g_variant_builder_add(&b, "{sv}", "EmergencyNumbers", g_variant_builder_end(&n)); - return g_variant_builder_end(&b); -} - -// oFono's StartTime is an ISO 8601 string; imsd hands us epoch seconds. -std::string StartTimeOf(std::int64_t epoch) { - if (epoch <= 0) return ""; - GDateTime* dt = g_date_time_new_from_unix_utc(epoch); - if (!dt) return ""; - gchar* s = g_date_time_format_iso8601(dt); - std::string out = s ? s : ""; - g_free(s); - g_date_time_unref(dt); - return out; -} - -// Deliberately no "Emergency" key: imsd owns emergency classification and does -// not export its per-call verdict, and a second guess here could disagree with -// the INVITE that actually went out. -GVariant* CallProps(const CallRec& r) { - GVariantBuilder b; - g_variant_builder_init(&b, G_VARIANT_TYPE("a{sv}")); - g_variant_builder_add(&b, "{sv}", "LineIdentification", g_variant_new_string(r.number.c_str())); - g_variant_builder_add(&b, "{sv}", "Name", g_variant_new_string("")); - g_variant_builder_add(&b, "{sv}", "State", g_variant_new_string(r.state.c_str())); - g_variant_builder_add(&b, "{sv}", "Multiparty", g_variant_new_boolean(FALSE)); - g_variant_builder_add(&b, "{sv}", "RemoteHeld", g_variant_new_boolean(FALSE)); - g_variant_builder_add(&b, "{sv}", "RemoteMultiparty", g_variant_new_boolean(FALSE)); - std::string started = StartTimeOf(r.startedAt); - if (!started.empty()) - g_variant_builder_add(&b, "{sv}", "StartTime", g_variant_new_string(started.c_str())); - return g_variant_builder_end(&b); -} - -GVariant* CallVector() { - GVariantBuilder b; - g_variant_builder_init(&b, G_VARIANT_TYPE("a(oa{sv})")); - for (const auto& [uni, rec] : Calls) - g_variant_builder_add(&b, "(o@a{sv})", rec.path.c_str(), CallProps(rec)); - return g_variant_builder_end(&b); -} - -// ---- imsd (client side) ----------------------------------------------------- -// Fire-and-forget into imsd; a dead imsd shows up via the name watch, so a -// failure here is logged rather than fatal (same contract as imsd-dialerd). -void CallImsd(const char* method, GVariant* params) { - g_dbus_connection_call( - Imsd, ImsdBus, ImsdPath, ImsdIface, method, params, nullptr, - G_DBUS_CALL_FLAGS_NONE, 5000, nullptr, - [](GObject* src, GAsyncResult* res, gpointer m) { - GError* err = nullptr; - GVariant* r = g_dbus_connection_call_finish(G_DBUS_CONNECTION(src), res, &err); - if (err) { - Log(std::format("{} failed: {}", static_cast(m), err->message)); - g_error_free(err); - } - if (r) g_variant_unref(r); - }, - const_cast(method)); -} - -std::string ActiveUni() { - for (const auto& [uni, rec] : Calls) - if (rec.state != "disconnected") return uni; - return ""; -} - -// ---- the VoiceCall objects -------------------------------------------------- -constexpr const char* CallXml = R"xml( - - - - - - - - - -)xml"; - -void HandleCallMethod(GDBusConnection*, const gchar*, const gchar* objectPath, const gchar*, const gchar* method, GVariant*, GDBusMethodInvocation* inv, gpointer) { - auto byPath = ByPath.find(objectPath ? objectPath : ""); - if (byPath == ByPath.end()) { - g_dbus_method_invocation_return_dbus_error(inv, ErrFailed, "no such call"); - return; - } - auto it = Calls.find(byPath->second); - if (it == Calls.end()) { - g_dbus_method_invocation_return_dbus_error(inv, ErrFailed, "no such call"); - return; - } - std::string_view m = method; - if (m == "GetProperties") { - g_dbus_method_invocation_return_value(inv, g_variant_new("(@a{sv})", CallProps(it->second))); - return; - } - if (m == "Answer") { - Log(std::format("Answer({})", it->second.uni)); - CallImsd("Accept", g_variant_new("(s)", it->second.uni.c_str())); - g_dbus_method_invocation_return_value(inv, nullptr); - return; - } - if (m == "Hangup") { - Log(std::format("Hangup({})", it->second.uni)); - CallImsd("HangUp", g_variant_new("(s)", it->second.uni.c_str())); - g_dbus_method_invocation_return_value(inv, nullptr); - return; - } - if (m == "Deflect") { - g_dbus_method_invocation_return_dbus_error(inv, ErrNotImplemented, "call deflection is not implemented"); - return; - } - g_dbus_method_invocation_return_dbus_error(inv, "org.freedesktop.DBus.Error.UnknownMethod", "no such method"); -} - -const GDBusInterfaceVTable CallVtable = { HandleCallMethod, nullptr, nullptr, {} }; - -guint RegisterObject(const char* xml, const char* path, const GDBusInterfaceVTable* vtable) { - GDBusNodeInfo* node = g_dbus_node_info_new_for_xml(xml, nullptr); - guint id = g_dbus_connection_register_object(Bus, path, node->interfaces[0], vtable, nullptr, nullptr, nullptr); - g_dbus_node_info_unref(node); - return id; -} - -// Allocate (or recall) the object path for an imsd call uni. imsd's unis -// ("ims-call-3") are not valid object-path elements, and a Dial reply can beat -// the CallAdded signal, so the mapping is created on first mention either way. -const std::string& PathFor(const std::string& uni) { - auto it = UniToPath.find(uni); - if (it != UniToPath.end()) return it->second; - std::string path = std::format("{}/voicecall{:02}", ModemPath, ++PathSeq); - ByPath[path] = uni; - return UniToPath.emplace(uni, std::move(path)).first->second; -} - -void ForgetPath(const std::string& uni) { - auto it = UniToPath.find(uni); - if (it == UniToPath.end()) return; - ByPath.erase(it->second); - UniToPath.erase(it); -} - -void AddCall(const std::string& uni, const std::string& number, std::string_view imsdState, std::int64_t startedAt) { - if (uni.empty() || Calls.contains(uni)) return; - const char* st = OfonoState(imsdState); - CallRec rec; - rec.uni = uni; - rec.path = PathFor(uni); - rec.number = number; - rec.state = st ? st : "dialing"; - rec.inbound = rec.state == "incoming"; - rec.startedAt = startedAt ? startedAt : g_get_real_time() / G_USEC_PER_SEC; - rec.regId = RegisterObject(CallXml, rec.path.c_str(), &CallVtable); - Log(std::format("call added {} {} {} [{}]", rec.path, rec.inbound ? "<-" : "->", rec.number, rec.state)); - GVariant* props = CallProps(rec); - Calls[uni] = std::move(rec); - Emit(ModemPath, VcmIface, "CallAdded", g_variant_new("(o@a{sv})", Calls[uni].path.c_str(), props)); -} - -void SetCallState(CallRec& rec, const char* state) { - if (rec.state == state) return; - rec.state = state; - Emit(rec.path.c_str(), CallIface, "PropertyChanged", - PropChanged("State", g_variant_new_string(state))); -} - -// The plugin caches DisconnectReason and reads it when CallRemoved lands, so -// the order here is load-bearing: disconnected state, then reason, then removal. -void RemoveCall(const std::string& uni, const char* reasonHint) { - auto it = Calls.find(uni); - if (it == Calls.end()) return; - CallRec& rec = it->second; - SetCallState(rec, "disconnected"); - const char* reason = !rec.reason.empty() ? rec.reason.c_str() - : (reasonHint ? reasonHint : "network"); - Emit(rec.path.c_str(), CallIface, "DisconnectReason", g_variant_new("(s)", reason)); - if (rec.regId) g_dbus_connection_unregister_object(Bus, rec.regId); - std::string path = rec.path; - Log(std::format("call removed {} ({})", path, reason)); - Calls.erase(it); - ForgetPath(uni); - Emit(ModemPath, VcmIface, "CallRemoved", g_variant_new("(o)", path.c_str())); -} - -// ---- Manager / Modem / VoiceCallManager objects ----------------------------- -constexpr const char* ManagerXml = R"xml( - - - - - - -)xml"; - -constexpr const char* ModemXml = R"xml( - - - - - - - - - -)xml"; - -constexpr const char* VcmXml = R"xml( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -)xml"; - -// USSD has no path through the IMS stack. The interface is answered (Calls -// proxies it unconditionally, and a legible error beats UnknownInterface) but -// deliberately NOT advertised in the modem's Interfaces list. -constexpr const char* SsXml = R"xml( - - - - - - - - - - - - - - - - - -)xml"; - -void HandleManagerMethod(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* method, GVariant*, GDBusMethodInvocation* inv, gpointer) { - if (std::string_view(method) == "GetModems") { - GVariantBuilder b; - g_variant_builder_init(&b, G_VARIANT_TYPE("a(oa{sv})")); - g_variant_builder_add(&b, "(o@a{sv})", ModemPath, ModemProps()); - g_dbus_method_invocation_return_value(inv, g_variant_new("(@a(oa{sv}))", g_variant_builder_end(&b))); - return; - } - g_dbus_method_invocation_return_dbus_error(inv, "org.freedesktop.DBus.Error.UnknownMethod", "no such method"); -} - -void HandleModemMethod(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* method, GVariant* params, GDBusMethodInvocation* inv, gpointer) { - std::string_view m = method; - if (m == "GetProperties") { - g_dbus_method_invocation_return_value(inv, g_variant_new("(@a{sv})", ModemProps())); - return; - } - if (m == "SetProperty") { - const gchar* prop = nullptr; - GVariant* value = nullptr; - g_variant_get(params, "(&sv)", &prop, &value); - if (value) g_variant_unref(value); - // Powered/Online track imsd's registration; nothing here is settable. - g_dbus_method_invocation_return_dbus_error( - inv, ErrNotSupported, - std::format("{} is driven by imsd's registration and cannot be set", prop ? prop : "?").c_str()); - return; - } - g_dbus_method_invocation_return_dbus_error(inv, "org.freedesktop.DBus.Error.UnknownMethod", "no such method"); -} - -void OnDialReply(GObject* src, GAsyncResult* res, gpointer user) { - GDBusMethodInvocation* inv = static_cast(user); - GError* err = nullptr; - GVariant* r = g_dbus_connection_call_finish(G_DBUS_CONNECTION(src), res, &err); - if (!r) { - Log(std::format("imsd Dial failed: {}", err ? err->message : "?")); - g_dbus_method_invocation_return_dbus_error(inv, ErrFailed, err ? err->message : "imsd Dial failed"); - if (err) g_error_free(err); - return; - } - const gchar* uni = nullptr; - g_variant_get(r, "(&s)", &uni); - if (!uni || !*uni) { - g_variant_unref(r); - g_dbus_method_invocation_return_dbus_error(inv, ErrFailed, "imsd returned no call id"); - return; - } - const std::string& path = PathFor(uni); - g_dbus_method_invocation_return_value(inv, g_variant_new("(o)", path.c_str())); - g_variant_unref(r); -} - -void HandleVcmMethod(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* method, GVariant* params, GDBusMethodInvocation* inv, gpointer) { - std::string_view m = method; - if (m == "GetProperties") { - g_dbus_method_invocation_return_value(inv, g_variant_new("(@a{sv})", VcmProps())); - return; - } - if (m == "GetCalls") { - g_dbus_method_invocation_return_value(inv, g_variant_new("(@a(oa{sv}))", CallVector())); - return; - } - if (m == "Dial") { - const gchar* number = nullptr; - const gchar* clir = nullptr; - g_variant_get(params, "(&s&s)", &number, &clir); - if (!number || !*number) { - g_dbus_method_invocation_return_dbus_error(inv, ErrInvalidArgs, "empty number"); - return; - } - // "default" is the only CLIR setting imsd can honour. Failing loudly on - // a withhold request beats dialling with the caller ID exposed. - std::string_view hide = clir ? clir : ""; - if (!hide.empty() && hide != "default") { - g_dbus_method_invocation_return_dbus_error(inv, ErrNotImplemented, "per-call caller-ID control is not implemented"); - return; - } - if (!VoiceUp()) { - g_dbus_method_invocation_return_dbus_error(inv, ErrFailed, "imsd is not registered"); - return; - } - Log(std::format("Dial({})", number)); - g_dbus_connection_call(Imsd, ImsdBus, ImsdPath, ImsdIface, "Dial", - g_variant_new("(s)", number), G_VARIANT_TYPE("(s)"), - G_DBUS_CALL_FLAGS_NONE, 10000, nullptr, OnDialReply, inv); - return; - } - if (m == "HangupAll") { - for (const auto& [uni, rec] : Calls) - if (rec.state != "disconnected") - CallImsd("HangUp", g_variant_new("(s)", uni.c_str())); - g_dbus_method_invocation_return_value(inv, nullptr); - return; - } - if (m == "SendTones") { - const gchar* tones = nullptr; - g_variant_get(params, "(&s)", &tones); - std::string target = ActiveUni(); - if (target.empty()) { - g_dbus_method_invocation_return_dbus_error(inv, ErrFailed, "no active call"); - return; - } - CallImsd("SendDtmf", g_variant_new("(ss)", target.c_str(), tones ? tones : "")); - g_dbus_method_invocation_return_value(inv, nullptr); - return; - } - // Single-call stack: no hold, no multiparty, no transfer, no dial memory. - if (m == "Transfer" || m == "SwapCalls" || m == "ReleaseAndAnswer" || m == "ReleaseAndSwap" - || m == "HoldAndAnswer" || m == "PrivateChat" || m == "CreateMultiparty" - || m == "HangupMultiparty" || m == "DialLast" || m == "DialMemory") { - g_dbus_method_invocation_return_dbus_error(inv, ErrNotImplemented, "not implemented by imsd"); - return; - } - g_dbus_method_invocation_return_dbus_error(inv, "org.freedesktop.DBus.Error.UnknownMethod", "no such method"); -} - -void HandleSsMethod(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* method, GVariant*, GDBusMethodInvocation* inv, gpointer) { - std::string_view m = method; - if (m == "GetProperties") { - GVariantBuilder b; - g_variant_builder_init(&b, G_VARIANT_TYPE("a{sv}")); - g_variant_builder_add(&b, "{sv}", "State", g_variant_new_string("idle")); - g_dbus_method_invocation_return_value(inv, g_variant_new("(@a{sv})", g_variant_builder_end(&b))); - return; - } - if (m == "Cancel") { - g_dbus_method_invocation_return_value(inv, nullptr); - return; - } - g_dbus_method_invocation_return_dbus_error(inv, ErrNotSupported, "USSD is not supported on the IMS stack"); -} - -const GDBusInterfaceVTable ManagerVtable = { HandleManagerMethod, nullptr, nullptr, {} }; -const GDBusInterfaceVTable ModemVtable = { HandleModemMethod, nullptr, nullptr, {} }; -const GDBusInterfaceVTable VcmVtable = { HandleVcmMethod, nullptr, nullptr, {} }; -const GDBusInterfaceVTable SsVtable = { HandleSsMethod, nullptr, nullptr, {} }; - -// ---- imsd events ------------------------------------------------------------ -void PublishInterfaces() { - Emit(ModemPath, ModemIface, "PropertyChanged", PropChanged("Interfaces", InterfacesValue())); - Emit(ModemPath, ModemIface, "PropertyChanged", - PropChanged("Online", g_variant_new_boolean(VoiceUp() ? TRUE : FALSE))); - Log(std::format("voice interface {}", VoiceUp() ? "available" : "withdrawn")); -} - -void OnImsdSignal(GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* signal, GVariant* params, gpointer) { - std::string_view sig = signal; - if (sig == "CallAdded") { - const gchar* uni = nullptr; - GVariant* info = nullptr; - g_variant_get(params, "(&s@a{sv})", &uni, &info); - AddCall(uni ? uni : "", DictStr(info, "number"), - DictStr(info, "state").empty() - ? (DictStr(info, "direction") == "incoming" ? "incoming" : "dialing") - : DictStr(info, "state"), - DictInt64(info, "startedAt")); - g_variant_unref(info); - } else if (sig == "CallStateChanged") { - const gchar* uni = nullptr; - const gchar* state = nullptr; - const gchar* reason = nullptr; - g_variant_get(params, "(&s&s&s)", &uni, &state, &reason); - auto it = Calls.find(uni ? uni : ""); - if (it == Calls.end()) return; - const char* mapped = OfonoState(state ? state : ""); - if (!mapped) return; - if (std::string_view(mapped) == "disconnected") { - // Bank the reason before the state flips: RemoveCall emits it. - it->second.reason = OfonoReason(reason ? reason : ""); - SetCallState(it->second, mapped); - Log(std::format("{} -> disconnected ({})", it->second.path, it->second.reason)); - return; - } - Log(std::format("{} -> {}", it->second.path, mapped)); - SetCallState(it->second, mapped); - } else if (sig == "CallDeleted") { - const gchar* uni = nullptr; - g_variant_get(params, "(&s)", &uni); - RemoveCall(uni ? uni : "", "network"); - } else if (sig == "RegistrationChanged") { - gboolean reg = FALSE; - g_variant_get(params, "(b)", ®); - if (Registered == static_cast(reg)) return; - Registered = reg; - PublishInterfaces(); - } -} - -void RefreshFromImsd() { - GError* err = nullptr; - GVariant* st = g_dbus_connection_call_sync(Imsd, ImsdBus, ImsdPath, ImsdIface, "GetStatus", nullptr, G_VARIANT_TYPE("(a{sv})"), G_DBUS_CALL_FLAGS_NONE, 5000, nullptr, &err); - if (!st) { - Log(std::format("imsd status query failed: {}", err ? err->message : "?")); - if (err) g_error_free(err); - Registered = false; - return; - } - GVariant* dict = g_variant_get_child_value(st, 0); - Registered = DictBool(dict, "registered"); - g_variant_unref(dict); - g_variant_unref(st); - - GVariant* calls = g_dbus_connection_call_sync(Imsd, ImsdBus, ImsdPath, ImsdIface, "GetCalls", nullptr, G_VARIANT_TYPE("(aa{sv})"), G_DBUS_CALL_FLAGS_NONE, 5000, nullptr, &err); - if (!calls) { - if (err) g_error_free(err); - return; - } - GVariant* arr = g_variant_get_child_value(calls, 0); - GVariantIter it; - g_variant_iter_init(&it, arr); - while (GVariant* info = g_variant_iter_next_value(&it)) { - AddCall(DictStr(info, "uni"), DictStr(info, "number"), DictStr(info, "state"), DictInt64(info, "startedAt")); - g_variant_unref(info); - } - g_variant_unref(arr); - g_variant_unref(calls); -} - -void OnImsdAppeared(GDBusConnection*, const gchar*, const gchar*, gpointer) { - if (ImsdUp) return; - ImsdUp = true; - Log("imsd up"); - RefreshFromImsd(); - PublishInterfaces(); -} - -void OnImsdVanished(GDBusConnection*, const gchar*, gpointer) { - if (!ImsdUp) return; - ImsdUp = false; - Registered = false; - Log("imsd down"); - std::vector live; - for (const auto& [uni, rec] : Calls) live.push_back(uni); - for (const std::string& uni : live) RemoveCall(uni, "network"); - PublishInterfaces(); -} - -gboolean OnTerm(gpointer loop) { - g_main_loop_quit(static_cast(loop)); - return G_SOURCE_REMOVE; -} - -void LoadEmergencyNumbers() { - EmergencyNumbers = { "112", "911" }; - std::string extra = EnvOr("EMERGENCY_NUMBERS", ""); - for (std::size_t pos = 0; pos <= extra.size();) { - std::size_t comma = extra.find(',', pos); - std::size_t end = comma == std::string::npos ? extra.size() : comma; - std::string n = extra.substr(pos, end - pos); - std::erase(n, ' '); - if (!n.empty() && std::ranges::find(EmergencyNumbers, n) == EmergencyNumbers.end()) - EmergencyNumbers.push_back(std::move(n)); - if (comma == std::string::npos) break; - pos = comma + 1; - } -} - -} // namespace - -int main(int argc, char** argv) { - bool session = false; - for (int i = 1; i < argc; i++) - if (std::string_view(argv[i]) == "--session") session = true; - - GBusType ofonoBus = session ? G_BUS_TYPE_SESSION : G_BUS_TYPE_SYSTEM; - std::string imsdBusName = EnvOr("IMSD_BUS", session ? "session" : "system"); - GBusType imsdBus = imsdBusName == "session" ? G_BUS_TYPE_SESSION : G_BUS_TYPE_SYSTEM; - - LoadEmergencyNumbers(); - - GError* err = nullptr; - Bus = g_bus_get_sync(ofonoBus, nullptr, &err); - if (!Bus) { - std::println(std::cerr, "imsd-ofonod: no {} bus: {}", session ? "session" : "system", err ? err->message : "?"); - return 1; - } - Imsd = imsdBus == ofonoBus ? Bus : g_bus_get_sync(imsdBus, nullptr, &err); - if (!Imsd) { - std::println(std::cerr, "imsd-ofonod: no {} bus for imsd: {}", imsdBusName, err ? err->message : "?"); - return 1; - } - - RegisterObject(ManagerXml, ManagerPath, &ManagerVtable); - RegisterObject(ModemXml, ModemPath, &ModemVtable); - RegisterObject(VcmXml, ModemPath, &VcmVtable); - RegisterObject(SsXml, ModemPath, &SsVtable); - - Loop = g_main_loop_new(nullptr, FALSE); - - // DO_NOT_QUEUE: a real ofonod already owning the name means two stacks are - // fighting over the modem — fail loudly instead of waiting our turn. - guint owner = g_bus_own_name_on_connection( - Bus, OfonoBus, G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE, - [](GDBusConnection*, const gchar* n, gpointer) { Log(std::format("owning {}", n)); }, - [](GDBusConnection*, const gchar* n, gpointer lp) { - Log(std::format("lost {} (is ofonod running?) — exiting", n)); - g_main_loop_quit(static_cast(lp)); - }, - Loop, nullptr); - - g_dbus_connection_signal_subscribe(Imsd, ImsdBus, ImsdIface, nullptr, ImsdPath, nullptr, G_DBUS_SIGNAL_FLAGS_NONE, OnImsdSignal, nullptr, nullptr); - g_bus_watch_name_on_connection(Imsd, ImsdBus, G_BUS_NAME_WATCHER_FLAGS_NONE, OnImsdAppeared, OnImsdVanished, nullptr, nullptr); - - g_unix_signal_add(SIGTERM, OnTerm, Loop); - g_unix_signal_add(SIGINT, OnTerm, Loop); - - Log(std::format("up (org.ofono on the {} bus, imsd on the {} bus)", session ? "session" : "system", imsdBusName)); - g_main_loop_run(Loop); - g_bus_unown_name(owner); - g_main_loop_unref(Loop); - return 0; -} diff --git a/packaging/90_imsd-ofono.gschema.override b/packaging/90_imsd-ofono.gschema.override deleted file mode 100644 index b609f3a..0000000 --- a/packaging/90_imsd-ofono.gschema.override +++ /dev/null @@ -1,17 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-only -# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® -# -# GNOME Calls autoloads ["mm", "sip"] by default. imsd is reached through the -# `ofono` provider (imsd-ofonod), so point Calls at that one instead. -# -# "mm" is dropped on purpose rather than appended: it would add the real -# ModemManager modem as a second, competing origin, and on a phone whose voice -# path is imsd the modem's own CS/voice route carries no audio — a dial that -# picks that origin fails silently. Re-add it if your device has a working -# ModemManager voice path and you want both. -# -# Install: /usr/share/glib-2.0/schemas/90_imsd-ofono.gschema.override -# then run glib-compile-schemas /usr/share/glib-2.0/schemas (the distro's -# package trigger normally does this for you). -[org.gnome.Calls] -autoload-plugins=['ofono'] diff --git a/packaging/APKBUILD b/packaging/APKBUILD index a048ea7..9a7e429 100644 --- a/packaging/APKBUILD +++ b/packaging/APKBUILD @@ -19,11 +19,6 @@ makedepends="clang lld libc++-dev llvm-libunwind-dev llvm-runtimes glib-dev pkgc # requests, which races imsd for the PDN and flaps it with a new prefix every # ~2.5 min — the two IMS stacks cannot share one PDN provides="81voltd=$pkgver-r$pkgrel" -# imsd-ofono is opt-in: it carries the org.ofono shim that lets GNOME Calls -# (Phosh, GNOME Mobile) drive imsd, plus the gschema override that repoints -# Calls at the ofono provider. A Plasma Mobile phone wants imsd-dialerd, which -# is in the main package, and must NOT get that override. -subpackages="$pkgname-ofono:ofono" source="$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz" builddir="$srcdir/$pkgname" @@ -38,12 +33,3 @@ check() { package() { make install DESTDIR="$pkgdir" } - -ofono() { - pkgdesc="GNOME Calls (Phosh, GNOME Mobile) backend for imsd" - depends="$pkgname=$pkgver-r$pkgrel calls" - amove usr/bin/imsd-ofonod - amove usr/lib/systemd/system/imsd-ofonod.service - amove usr/share/dbus-1/system.d/imsd-ofono.conf - amove usr/share/glib-2.0/schemas/90_imsd-ofono.gschema.override -} diff --git a/packaging/APKBUILD.binary b/packaging/APKBUILD.binary index 97a588f..0043af0 100644 --- a/packaging/APKBUILD.binary +++ b/packaging/APKBUILD.binary @@ -24,11 +24,8 @@ depends="modemmanager libc++ opencore-amr vo-amrwbenc pipewire-tools" # (fp6 journal/ims.md s57) — the two IMS stacks cannot share one PDN 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. -# see packaging/APKBUILD: the org.ofono shim for GNOME Calls is opt-in -# (ofono before systemd: systemd() amoves all of usr/lib/systemd/system, -# so ofono() must take imsd-ofonod.service first) -subpackages="$pkgname-ofono:ofono $pkgname-systemd" +# tested under systemd; an initd is welcome once someone can verify one +subpackages="$pkgname-systemd" options="!check !tracedeps" source=" imsd-$pkgver.tar.gz @@ -63,22 +60,6 @@ package() { mkdir -p "$pkgdir"/usr/lib/systemd/system/imsd.service.d printf '[Unit]\nConditionPathExists=/etc/imsd.env\n' \ > "$pkgdir"/usr/lib/systemd/system/imsd.service.d/10-require-config.conf - install -Dm755 imsd-ofonod "$pkgdir"/usr/bin/imsd-ofonod - install -Dm644 imsd-ofonod.service \ - "$pkgdir"/usr/lib/systemd/system/imsd-ofonod.service - install -Dm644 org.ofono.conf \ - "$pkgdir"/usr/share/dbus-1/system.d/imsd-ofono.conf - install -Dm644 90_imsd-ofono.gschema.override \ - "$pkgdir"/usr/share/glib-2.0/schemas/90_imsd-ofono.gschema.override -} - -ofono() { - pkgdesc="GNOME Calls (Phosh, GNOME Mobile) backend for imsd" - depends="$pkgname=$pkgver-r$pkgrel calls" - amove usr/bin/imsd-ofonod - amove usr/lib/systemd/system/imsd-ofonod.service - amove usr/share/dbus-1/system.d/imsd-ofono.conf - amove usr/share/glib-2.0/schemas/90_imsd-ofono.gschema.override } systemd() { diff --git a/packaging/build-package.sh b/packaging/build-package.sh index 75a0aeb..ae45bf6 100755 --- a/packaging/build-package.sh +++ b/packaging/build-package.sh @@ -83,7 +83,6 @@ XTARGET="--target=aarch64-alpine-linux-musl --sysroot=$SYSROOT --march=armv8.6-a crafter-build -- $XTARGET crafter-build -- --product=media $XTARGET crafter-build -- --product=dialerd $XTARGET -crafter-build -- --product=ofonod $XTARGET crafter-build test # --- bundle + package diff --git a/packaging/ims-pdn-up.sh b/packaging/ims-pdn-up.sh index 3ba1603..5c50877 100644 --- a/packaging/ims-pdn-up.sh +++ b/packaging/ims-pdn-up.sh @@ -58,73 +58,37 @@ while :; do done log "modem $MODEM" -# ---- the ims profile index (WDS profile list), unless configured. The -# lookup itself can fail while ModemManager is still enabling the modem; a -# failed lookup is not "no profile" (the APN-string request it would fall -# back to is the one the modem refuses under an IPv4 attach), so it is -# retried after the registration wait and only a list without the APN -# falls back to the APN string. -LOOKUP=pending -if [ -n "$PROFILE_ID" ]; then - [ "$PROFILE_ID" = none ] && PROFILE_ID= - LOOKUP=done -fi -lookup_profile() { # $1 = log suffix; sets PROFILE_ID, LOOKUP=done on a usable list - [ "$LOOKUP" = done ] && return 0 - if OUT=$(qmicli -d qrtr://0 --wds-get-profile-list=3gpp 2>&1); then - PROFILE_ID=$(printf '%s\n' "$OUT" | awk -v apn="$IMS_APN" ' - /^[ \t]*\[[0-9]+\] 3gpp/ { idx = $1; gsub(/[^0-9]/, "", idx) } - /APN:/ { a = $0; sub(/.*APN: '"'"'/, "", a); sub(/'"'"'.*/, "", a); - if (tolower(a) == tolower(apn) && idx != "") { print idx; exit } }') - LOOKUP=done - [ -n "$PROFILE_ID" ] && log "ims profile: index $PROFILE_ID (apn $IMS_APN)" || log "ims profile: none for apn $IMS_APN, connecting by APN string" - else - log "ims profile lookup failed$1: $(squash "$OUT")" - fi +# ---- the ims profile index (WDS profile list), unless configured +ims_profile_index() { # $1 = apn + qmicli -d qrtr://0 --wds-get-profile-list=3gpp 2>/dev/null | awk -v apn="$1" ' + /^[ \t]*\[[0-9]+\] 3gpp/ { idx = $1; gsub(/[^0-9]/, "", idx) } + /APN:/ { a = $0; sub(/.*APN: '"'"'/, "", a); sub(/'"'"'.*/, "", a); + if (tolower(a) == tolower(apn) && idx != "") { print idx; exit } }' } -lookup_profile " (modem state: $(modem_state))" +if [ -z "$PROFILE_ID" ]; then + PROFILE_ID=$(ims_profile_index "$IMS_APN") + [ -n "$PROFILE_ID" ] && log "ims profile: index $PROFILE_ID (apn $IMS_APN)" || log "ims profile: none for apn $IMS_APN, connecting by APN string" +elif [ "$PROFILE_ID" = none ]; then + PROFILE_ID= +fi -# ---- find a connected ims bearer; else find-or-create one and connect it. -# APN names are case-insensitive. A DISCONNECTED bearer is reused only when -# it is the request this run would make (same profile index, or same APN when -# connecting by APN string): a leftover APN-string bearer from a run whose -# lookup failed is the walled request itself, and reconnecting it fails on -# every restart until the modem is reset (field: O2 UK, 2026-09-20). -has() { printf '%s\n' "$INFO" | grep -qi "^bearer\.$1 *: *$2\$"; } -find_ims_bearer() { # $1 = required bearer.status.connected value; $2 = "relaxed" ignores ip-type +# ---- find a connected ims bearer; else find-or-create one and connect it +find_ims_bearer() { # $1 = required bearer.status.connected value for B in $(bearer_paths); do INFO=$(kv -b "$B") || continue - has status.connected "$1" || continue if [ -n "$PROFILE_ID" ]; then - if [ "$1" = yes ]; then - has properties.profile-id "$PROFILE_ID" || has properties.apn "$IMS_APN" || continue - else - has properties.profile-id "$PROFILE_ID" || continue - fi + echo "$INFO" | grep -q "^bearer\.properties\.profile-id *: *$PROFILE_ID\$" || + echo "$INFO" | grep -q "^bearer\.properties\.apn *: *$IMS_APN\$" || continue else - has properties.apn "$IMS_APN" || continue + echo "$INFO" | grep -q "^bearer\.properties\.apn *: *$IMS_APN\$" || continue fi - [ "$2" = relaxed ] || has properties.ip-type "$IP_TYPE" || continue + echo "$INFO" | grep -q "^bearer\.properties\.ip-type *: *$IP_TYPE\$" || continue + echo "$INFO" | grep -q "^bearer\.status\.connected *: *$1\$" || continue echo "$B" return 0 done return 1 } -# leftover disconnected APN-string ims bearers when this run connects by -# profile index: never reused (above), deleted so a later run without an -# index cannot pick one up either -delete_stale_bearers() { - [ -n "$PROFILE_ID" ] || return 0 - for B in $(bearer_paths); do - INFO=$(kv -b "$B") || continue - has status.connected no || continue - has properties.apn "$IMS_APN" || continue - has properties.profile-id "$PROFILE_ID" && continue - OUT=$(mmcli -m "$MODEM" --delete-bearer="$B" 2>&1) && - log "deleted stale bearer $B (apn $IMS_APN, no profile index)" || - log "could not delete stale bearer $B: $(squash "$OUT")" - done -} BEARER=$(find_ims_bearer yes) @@ -147,26 +111,13 @@ if [ -z "$BEARER" ]; then waited=$((waited + 5)) done log "modem state: $(modem_state), packet service: $(packet_state)" - # a lookup that failed while the modem was still coming up - n=0 - while [ "$LOOKUP" != done ] && [ "$n" -lt 3 ]; do - n=$((n + 1)) - sleep 5 - lookup_profile " (retry $n)" - done - if [ "$LOOKUP" != done ]; then - PROFILE_ID= - log "ims profile: lookup keeps failing, connecting by APN string" - fi - BEARER=$(find_ims_bearer yes) # the index may now match a connected profile-indexed PDN - [ -n "$BEARER" ] || delete_stale_bearers fi n=0 while [ -z "$BEARER" ]; do n=$((n + 1)) [ "$n" -gt 10 ] && { log "bearer connect failed after 10 attempts"; exit 1; } - B=$(find_ims_bearer no) # reuse a stale disconnected ims bearer of this run's shape + B=$(find_ims_bearer no) # reuse a stale disconnected ims bearer if [ -z "$B" ]; then if [ -n "$PROFILE_ID" ]; then SPEC="profile-id=$PROFILE_ID,ip-type=$IP_TYPE" @@ -187,15 +138,6 @@ while [ -z "$BEARER" ]; do BEARER=$B else log "connect attempt $n failed (state: $(modem_state)): $(squash "$OUT"); retrying in 10 s" - case "$OUT" in *call-already-present*) - # the PDN is up on a bearer this run did not recognise (ip-type or - # APN spelling); use it rather than ask the modem for a second one - if B=$(find_ims_bearer yes relaxed); then - log "adopting the connected bearer $B" - BEARER=$B - continue - fi ;; - esac sleep 10 fi done diff --git a/packaging/imsd-ofonod.service b/packaging/imsd-ofonod.service deleted file mode 100644 index be308ee..0000000 --- a/packaging/imsd-ofonod.service +++ /dev/null @@ -1,25 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-only -# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® -[Unit] -Description=oFono-compatible GNOME Calls backend for imsd -Documentation=https://forgejo.catcrafts.net/Catcrafts/imsd -# org.ofono is a system-bus name, so this is a system service; it needs no -# privileges beyond owning that name. Only enable it on a GNOME/Phosh session -# — Plasma Mobile uses imsd-dialerd instead. Both may run at once (different -# buses, different consumers), but a real ofonod must not. -Conflicts=ofono.service -After=imsd.service -Wants=imsd.service - -[Service] -Type=simple -# EMERGENCY_NUMBERS only, and only so the oFono EmergencyNumbers property -# matches imsd's configuration; imsd itself owns emergency classification. -EnvironmentFile=-/etc/imsd.env -ExecStart=/usr/bin/imsd-ofonod -# it holds no call state of its own — imsd is re-queried on every reconnect -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target diff --git a/packaging/make-bin-tarball.sh b/packaging/make-bin-tarball.sh index 24e4ffe..03f7186 100755 --- a/packaging/make-bin-tarball.sh +++ b/packaging/make-bin-tarball.sh @@ -11,11 +11,9 @@ VER="${1:-$(sed -n 's/.*char\* Version = "\(.*\)".*/\1/p' implementations/main.c BIN=$(ls -t bin/imsd-aarch64-*/imsd 2>/dev/null | head -n1) MEDIA=$(ls -t bin/imsd-media-aarch64-*/imsd-media 2>/dev/null | head -n1) DIALERD=$(ls -t bin/imsd-dialerd-aarch64-*/imsd-dialerd 2>/dev/null | head -n1) -OFONOD=$(ls -t bin/imsd-ofonod-aarch64-*/imsd-ofonod 2>/dev/null | head -n1) [ -n "$BIN" ] || { echo "no aarch64 imsd build found — cross-compile first" >&2; exit 1; } [ -n "$MEDIA" ] || { echo "no aarch64 imsd-media build — cross-compile with --product=media" >&2; exit 1; } [ -n "$DIALERD" ] || { echo "no aarch64 imsd-dialerd build — cross-compile with --product=dialerd" >&2; exit 1; } -[ -n "$OFONOD" ] || { echo "no aarch64 imsd-ofonod build — cross-compile with --product=ofonod" >&2; exit 1; } stage=$(mktemp -d) trap 'rm -rf "$stage"' EXIT @@ -23,10 +21,8 @@ mkdir "$stage/imsd-$VER" cp "$BIN" "$stage/imsd-$VER/imsd" cp "$MEDIA" "$stage/imsd-$VER/imsd-media" cp "$DIALERD" "$stage/imsd-$VER/imsd-dialerd" -cp "$OFONOD" "$stage/imsd-$VER/imsd-ofonod" cp packaging/ims-pdn-up.sh packaging/imsd.service \ - packaging/imsd-dialerd.desktop packaging/imsd-ofonod.service \ - packaging/org.ofono.conf packaging/90_imsd-ofono.gschema.override \ + packaging/imsd-dialerd.desktop \ packaging/net.catcrafts.IMS1.conf "$stage/imsd-$VER/" tar -C "$stage" -czf "imsd-$VER.tar.gz" "imsd-$VER" echo "wrote imsd-$VER.tar.gz ($(du -h "imsd-$VER.tar.gz" | cut -f1))" diff --git a/packaging/org.ofono.conf b/packaging/org.ofono.conf deleted file mode 100644 index 0b73669..0000000 --- a/packaging/org.ofono.conf +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/project.cpp b/project.cpp index 3507130..d38bcac 100644 --- a/project.cpp +++ b/project.cpp @@ -58,7 +58,7 @@ static void ApplyGioFlags(Configuration& cfg) { } extern "C" Configuration CrafterBuildProject(std::span args) { - // Four executables come out of this repo; which one a build produces is + // Three executables come out of this repo; which one a build produces is // chosen by `--product=`: // daemon (default) — imsd, the control-plane daemon (GDBus + engine), // and the unit tests, which link imsd-core. @@ -68,32 +68,11 @@ extern "C" Configuration CrafterBuildProject(std::span a // dialerd — imsd-dialerd, the session-bus Plasma Dialer // backend (org.kde.telephony.*) bridging to imsd // (pure GDBus translation, no core). - // ofonod — imsd-ofonod, the system-bus GNOME Calls backend - // (org.ofono.*) bridging to imsd, for Phosh and - // GNOME Mobile (pure GDBus translation, no core). bool wantMedia = false; bool wantDialerd = false; - bool wantOfonod = false; for (std::string_view a : args) { if (a == "--product=media") wantMedia = true; if (a == "--product=dialerd") wantDialerd = true; - if (a == "--product=ofonod") wantOfonod = true; - } - if (wantOfonod) { - Configuration ofonod; - ofonod.path = "./"; - ofonod.name = "imsd-ofonod"; - ofonod.outputName = "imsd-ofonod"; - ApplyStandardArgs(ofonod, args); - ofonod.type = ConfigurationType::Executable; - { - std::array ifaces = {}; - std::array impls = { "implementations/ofonod" }; - ofonod.GetInterfacesAndImplementations(ifaces, impls); - } - ApplyGioFlags(ofonod); - ProjectLint::AddProjectLintRules(ofonod); - return ofonod; } if (wantDialerd) { Configuration dialerd;