Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
590c61fe0b |
|||
|
b986eb3750 |
|||
|
6689b8b252 |
13 changed files with 1117 additions and 35 deletions
21
Makefile
21
Makefile
|
|
@ -7,10 +7,11 @@
|
|||
# flags. Keep the two in sync when products, module partitions or link
|
||||
# flags change.
|
||||
#
|
||||
# make -> build/make/{imsd,imsd-media,imsd-dialerd}
|
||||
# make -> build/make/{imsd,imsd-media,imsd-dialerd,imsd-ofonod}
|
||||
# make check -> build + run the 7 unit-test suites
|
||||
# make install -> DESTDIR/PREFIX staged install (binaries + systemd
|
||||
# unit, D-Bus policy, dialerd autostart, ims-pdn-up.sh)
|
||||
# units, D-Bus policies, dialerd autostart, the GNOME
|
||||
# Calls plugin override, 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
|
||||
|
|
@ -54,7 +55,7 @@ CORE_OBJS = $(CORE_MODULES:%=$(OBJ)/%.o)
|
|||
|
||||
TESTS = Util Aka Ipsec Messages Engine Sip Sdp
|
||||
|
||||
all: $(O)/imsd $(O)/imsd-media $(O)/imsd-dialerd
|
||||
all: $(O)/imsd $(O)/imsd-media $(O)/imsd-dialerd $(O)/imsd-ofonod
|
||||
|
||||
# ---- module BMIs ----------------------------------------------------------
|
||||
|
||||
|
|
@ -103,6 +104,13 @@ $(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
|
||||
|
|
@ -121,6 +129,7 @@ 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
|
||||
|
|
@ -130,6 +139,12 @@ 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 $@
|
||||
|
|
|
|||
116
README.md
116
README.md
|
|
@ -9,8 +9,9 @@ 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 Plasma Mobile dialer can place and receive real VoLTE
|
||||
calls on a commercial network.
|
||||
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.
|
||||
|
||||
## Status
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ live only in the `imsd` daemon shell.
|
|||
|
||||
## Architecture
|
||||
|
||||
C++26 modules, built with Crafter Build. Four build products:
|
||||
C++26 modules, built with Crafter Build. Five 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
|
||||
|
|
@ -48,6 +49,26 @@ C++26 modules, built with Crafter Build. Four 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)
|
||||
|
||||
|
|
@ -85,7 +106,8 @@ gio-2.0 headers (`glib2` on Arch, `glib-dev` on Alpine).
|
|||
```sh
|
||||
crafter-build # bin/imsd-<target>-<march>/imsd (the daemon)
|
||||
crafter-build -- --product=media # bin/imsd-media-.../imsd-media (the media leg)
|
||||
crafter-build -- --product=dialerd # bin/imsd-dialerd-.../imsd-dialerd (dialer backend)
|
||||
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 test # unit tests (Util, Aka, Ipsec, Messages, Engine, Sip, Sdp)
|
||||
```
|
||||
|
||||
|
|
@ -97,7 +119,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}
|
||||
make # build/make/{imsd,imsd-media,imsd-dialerd,imsd-ofonod}
|
||||
make check # the same 7 unit-test suites
|
||||
make install # DESTDIR/PREFIX staged install incl. the packaging/ files
|
||||
```
|
||||
|
|
@ -134,6 +156,86 @@ 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
|
||||
|
|
@ -204,6 +306,9 @@ 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).
|
||||
|
|
@ -212,4 +317,3 @@ 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 |
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ import Imsd;
|
|||
namespace {
|
||||
|
||||
// ---- static config (env-overridable, same knobs as imsd.py) ---------------
|
||||
constexpr const char* Version = "0.3.6";
|
||||
constexpr const char* Version = "0.3.7";
|
||||
constexpr const char* BusName = "net.catcrafts.IMS1";
|
||||
constexpr const char* ObjPath = "/net/catcrafts/IMS1";
|
||||
constexpr const char* Iface = "net.catcrafts.IMS1";
|
||||
|
|
|
|||
770
implementations/ofonod.cpp
Normal file
770
implementations/ofonod.cpp
Normal file
|
|
@ -0,0 +1,770 @@
|
|||
// 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 <gio/gio.h>
|
||||
#include <glib-unix.h>
|
||||
|
||||
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<std::string> 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<std::string, CallRec> Calls; // uni -> record
|
||||
std::map<std::string, std::string> ByPath; // object path -> uni
|
||||
std::map<std::string, std::string> 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<const char*>(m), err->message));
|
||||
g_error_free(err);
|
||||
}
|
||||
if (r) g_variant_unref(r);
|
||||
},
|
||||
const_cast<char*>(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(
|
||||
<node>
|
||||
<interface name="org.ofono.VoiceCall">
|
||||
<method name="GetProperties"><arg type="a{sv}" name="properties" direction="out"/></method>
|
||||
<method name="Deflect"><arg type="s" name="number" direction="in"/></method>
|
||||
<method name="Hangup"/>
|
||||
<method name="Answer"/>
|
||||
<signal name="PropertyChanged"><arg type="s" name="name"/><arg type="v" name="value"/></signal>
|
||||
<signal name="DisconnectReason"><arg type="s" name="reason"/></signal>
|
||||
</interface>
|
||||
</node>)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(
|
||||
<node>
|
||||
<interface name="org.ofono.Manager">
|
||||
<method name="GetModems"><arg type="a(oa{sv})" name="modems" direction="out"/></method>
|
||||
<signal name="ModemAdded"><arg type="o" name="path"/><arg type="a{sv}" name="properties"/></signal>
|
||||
<signal name="ModemRemoved"><arg type="o" name="path"/></signal>
|
||||
</interface>
|
||||
</node>)xml";
|
||||
|
||||
constexpr const char* ModemXml = R"xml(
|
||||
<node>
|
||||
<interface name="org.ofono.Modem">
|
||||
<method name="GetProperties"><arg type="a{sv}" name="properties" direction="out"/></method>
|
||||
<method name="SetProperty">
|
||||
<arg type="s" name="property" direction="in"/>
|
||||
<arg type="v" name="value" direction="in"/>
|
||||
</method>
|
||||
<signal name="PropertyChanged"><arg type="s" name="name"/><arg type="v" name="value"/></signal>
|
||||
</interface>
|
||||
</node>)xml";
|
||||
|
||||
constexpr const char* VcmXml = R"xml(
|
||||
<node>
|
||||
<interface name="org.ofono.VoiceCallManager">
|
||||
<method name="GetProperties"><arg type="a{sv}" name="properties" direction="out"/></method>
|
||||
<method name="Dial">
|
||||
<arg type="s" name="number" direction="in"/>
|
||||
<arg type="s" name="hide_callerid" direction="in"/>
|
||||
<arg type="o" name="path" direction="out"/>
|
||||
</method>
|
||||
<method name="GetCalls"><arg type="a(oa{sv})" name="calls_with_properties" direction="out"/></method>
|
||||
<method name="HangupAll"/>
|
||||
<method name="SendTones"><arg type="s" name="tones" direction="in"/></method>
|
||||
<method name="Transfer"/>
|
||||
<method name="SwapCalls"/>
|
||||
<method name="ReleaseAndAnswer"/>
|
||||
<method name="ReleaseAndSwap"/>
|
||||
<method name="HoldAndAnswer"/>
|
||||
<method name="PrivateChat">
|
||||
<arg type="o" name="call" direction="in"/>
|
||||
<arg type="ao" name="calls" direction="out"/>
|
||||
</method>
|
||||
<method name="CreateMultiparty"><arg type="ao" name="calls" direction="out"/></method>
|
||||
<method name="HangupMultiparty"/>
|
||||
<method name="DialLast"/>
|
||||
<method name="DialMemory"><arg type="u" name="memory_location" direction="in"/></method>
|
||||
<signal name="PropertyChanged"><arg type="s" name="name"/><arg type="v" name="value"/></signal>
|
||||
<signal name="CallAdded"><arg type="o" name="path"/><arg type="a{sv}" name="properties"/></signal>
|
||||
<signal name="CallRemoved"><arg type="o" name="path"/></signal>
|
||||
<signal name="Forwarded"><arg type="s" name="type"/></signal>
|
||||
<signal name="BarringActive"><arg type="s" name="type"/></signal>
|
||||
</interface>
|
||||
</node>)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(
|
||||
<node>
|
||||
<interface name="org.ofono.SupplementaryServices">
|
||||
<method name="Initiate">
|
||||
<arg type="s" name="command" direction="in"/>
|
||||
<arg type="s" name="result_name" direction="out"/>
|
||||
<arg type="v" name="value" direction="out"/>
|
||||
</method>
|
||||
<method name="Respond">
|
||||
<arg type="s" name="reply" direction="in"/>
|
||||
<arg type="s" name="result" direction="out"/>
|
||||
</method>
|
||||
<method name="Cancel"/>
|
||||
<method name="GetProperties"><arg type="a{sv}" name="properties" direction="out"/></method>
|
||||
<signal name="NotificationReceived"><arg type="s" name="message"/></signal>
|
||||
<signal name="RequestReceived"><arg type="s" name="message"/></signal>
|
||||
<signal name="PropertyChanged"><arg type="s" name="name"/><arg type="v" name="value"/></signal>
|
||||
</interface>
|
||||
</node>)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<GDBusMethodInvocation*>(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<bool>(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<std::string> 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<GMainLoop*>(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<GMainLoop*>(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;
|
||||
}
|
||||
17
packaging/90_imsd-ofono.gschema.override
Normal file
17
packaging/90_imsd-ofono.gschema.override
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# 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']
|
||||
|
|
@ -19,6 +19,11 @@ 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"
|
||||
|
||||
|
|
@ -33,3 +38,12 @@ 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,11 @@ 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
|
||||
subpackages="$pkgname-systemd"
|
||||
# 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"
|
||||
options="!check !tracedeps"
|
||||
source="
|
||||
imsd-$pkgver.tar.gz
|
||||
|
|
@ -60,6 +63,22 @@ 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() {
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -58,37 +58,73 @@ while :; do
|
|||
done
|
||||
log "modem $MODEM"
|
||||
|
||||
# ---- 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 } }'
|
||||
}
|
||||
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=
|
||||
# ---- 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
|
||||
}
|
||||
lookup_profile " (modem state: $(modem_state))"
|
||||
|
||||
# ---- find a connected ims bearer; else find-or-create one and connect it
|
||||
find_ims_bearer() { # $1 = required bearer.status.connected value
|
||||
# ---- 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
|
||||
for B in $(bearer_paths); do
|
||||
INFO=$(kv -b "$B") || continue
|
||||
has status.connected "$1" || continue
|
||||
if [ -n "$PROFILE_ID" ]; then
|
||||
echo "$INFO" | grep -q "^bearer\.properties\.profile-id *: *$PROFILE_ID\$" ||
|
||||
echo "$INFO" | grep -q "^bearer\.properties\.apn *: *$IMS_APN\$" || continue
|
||||
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
|
||||
else
|
||||
echo "$INFO" | grep -q "^bearer\.properties\.apn *: *$IMS_APN\$" || continue
|
||||
has properties.apn "$IMS_APN" || continue
|
||||
fi
|
||||
echo "$INFO" | grep -q "^bearer\.properties\.ip-type *: *$IP_TYPE\$" || continue
|
||||
echo "$INFO" | grep -q "^bearer\.status\.connected *: *$1\$" || continue
|
||||
[ "$2" = relaxed ] || has properties.ip-type "$IP_TYPE" || 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)
|
||||
|
||||
|
|
@ -111,13 +147,26 @@ 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
|
||||
B=$(find_ims_bearer no) # reuse a stale disconnected ims bearer of this run's shape
|
||||
if [ -z "$B" ]; then
|
||||
if [ -n "$PROFILE_ID" ]; then
|
||||
SPEC="profile-id=$PROFILE_ID,ip-type=$IP_TYPE"
|
||||
|
|
@ -138,6 +187,15 @@ 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
|
||||
|
|
|
|||
25
packaging/imsd-ofonod.service
Normal file
25
packaging/imsd-ofonod.service
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# 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
|
||||
|
|
@ -11,9 +11,11 @@ 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
|
||||
|
|
@ -21,8 +23,10 @@ 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-dialerd.desktop packaging/imsd-ofonod.service \
|
||||
packaging/org.ofono.conf packaging/90_imsd-ofono.gschema.override \
|
||||
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))"
|
||||
|
|
|
|||
34
packaging/org.ofono.conf
Normal file
34
packaging/org.ofono.conf
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
|
||||
<!-- SPDX-License-Identifier: GPL-3.0-only -->
|
||||
<!-- SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® -->
|
||||
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||||
<!-- imsd-ofonod system-bus policy: imsd presented as an oFono modem, which is
|
||||
the seam GNOME Calls' bundled `ofono` provider plugin drives.
|
||||
Install: /usr/share/dbus-1/system.d/imsd-ofono.conf
|
||||
|
||||
Deliberately NOT named org.ofono.conf on disk: the real oFono package
|
||||
ships that filename, and the two must not silently overwrite each other.
|
||||
They cannot run together in any case — one org.ofono name, one owner. -->
|
||||
<busconfig>
|
||||
<policy user="root">
|
||||
<allow own="org.ofono"/>
|
||||
<!-- imsd (root) reaches this daemon only by broadcast signals, and
|
||||
dbus-broker checks a broadcast against the sender's send policy for
|
||||
every name the receiver owns (dbus-daemon does not): without this
|
||||
line the default deny below silently drops every CallAdded /
|
||||
CallStateChanged from imsd on the way to imsd-ofonod (found on the
|
||||
FP6, 2026-09-20). -->
|
||||
<allow send_destination="org.ofono"/>
|
||||
</policy>
|
||||
<!-- the session user's GNOME Calls drives the modem; call control is
|
||||
exactly what net.catcrafts.IMS1 already grants it through the dialerd -->
|
||||
<policy user="user">
|
||||
<allow send_destination="org.ofono"/>
|
||||
<allow receive_sender="org.ofono"/>
|
||||
</policy>
|
||||
<policy context="default">
|
||||
<deny own="org.ofono"/>
|
||||
<deny send_destination="org.ofono"/>
|
||||
</policy>
|
||||
</busconfig>
|
||||
23
project.cpp
23
project.cpp
|
|
@ -58,7 +58,7 @@ static void ApplyGioFlags(Configuration& cfg) {
|
|||
}
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||
// Three executables come out of this repo; which one a build produces is
|
||||
// Four 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,11 +68,32 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> 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<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "implementations/ofonod" };
|
||||
ofonod.GetInterfacesAndImplementations(ifaces, impls);
|
||||
}
|
||||
ApplyGioFlags(ofonod);
|
||||
ProjectLint::AddProjectLintRules(ofonod);
|
||||
return ofonod;
|
||||
}
|
||||
if (wantDialerd) {
|
||||
Configuration dialerd;
|
||||
|
|
|
|||
Loading…
Reference in a new issue