DIY Omarchy Linux Phone

Calls and SMS on the Omarchy OnePlus 6T

In the last post the Omarchy OnePlus 6T had LTE data, a cellular widget and a shell that behaves like a phone. Calls and SMS were next on the not-working list. SMS now works in both directions. Calls connect: the modem logs dialing, ringing-out, active. Audio on a connected call does not work yet. Everything is in omarchy-phone. Code available here.

A network with no plan B

The SIM I am testing with sits on Three UK, and the network it lands on has no circuit-switched fallback to lean on. That is the serving cell’s report, not a property of the SIM: qmicli --nas-get-serving-system says CS: 'detached' and PS: 'attached', and --nas-get-system-info says LTE Voice support: 'no', IMS voice support: 'yes'. Everything has to ride IMS, the IP subsystem that carries VoLTE and SMS over the data network.

With data working and the modem registered, none of the telephony worked. Outgoing calls went dialing then terminated after about 25 seconds. SMS submits timed out with the same 25-second timeout and no message-reference assigned. Inbound was quieter: calls from another handset went straight to voicemail with no call object at all, and Three queued every text sent to the SIM. Forcing the modem into a CS-capable RAT was refused (Unsupported: The given combination of allowed and preferred modes is not supported), so the modem could not be dropped onto a legacy rail either.

Probing the modem directly did not get far either. With ModemManager stopped, qmicli --wms-get-routes showed six routes, all store-and-notify, so inbound was set up to work; --ims-get-ims-services-enabled-setting failed with QMI error 70 (InvalidOperation), the modem exposing no IMS service query at all. I concluded the network had no route to this device. That conclusion was half wrong.

An iPhone can swallow the test. Sending to the number from an iPhone may route over iMessage and never touch the modem. The bubble has to be green and labelled SMS before a test means anything.

Where carriers switch off what

There was no plan B because the generations do not carry voice the same way, and carriers retired them at different speeds:

NetworkVoiceSMS
2GCircuit-switchedCircuit-switched
3GCircuit-switchedCircuit-switched
4GVoLTE, a SIP call over an IMS data connection, or CS fallback, where the network parks you on 2G or 3G for the length of the callOver IMS, or the same fallback
5GVoNR, the same IMS againOver IMS

On 2G and 3G the baseband says “dial” and the network does the rest; a dumb modem is enough. LTE has no circuit-switched domain at all. Which legacy rail is left is a per-carrier fact:

Where2G3G
GermanyKept, as the basic network for telephony and SMSOff in 2021
UK, except ThreeUntil at least 2029, under a 2033 switch-off deadlineOff by the end of 2025
Three UKNever builtOff in November 2025
USMostly gone years before the 3G shutdownsOff in 2022
AustraliaOff 2016 to 2018Off in 2024
SingaporeOff in 2017Off in 2024
JapanOff by 2012Last one off in March 2026

A German or EE SIM should need none of this post’s machinery: the network still runs the fallback rail. Whether this modem can register on it is untested; the one attempt to force a CS-capable mode was refused. Three never built 2G at all and switched 3G off; as of writing the fallback rail does not exist.

5G standalone carries voice over VoNR, which is the same IMS again, and every 2G switch-off date on the calendar removes the other rail. A mainline Linux phone either gets IMS right or has no telephony. Even IMS-correct is not always enough: carriers gate VoLTE by SIM provisioning and sometimes by device allowlist, and 81voltd’s author reports it working on Optus and not on Telstra. The only test that settles any of it is the modem’s own report.

81voltd

The missing piece is a daemon. The modem firmware wants an IMS data connection and asks the host to bring one up; nothing in a stock Linux stack answers. postmarketOS ships the answer: 81voltd, about 1400 lines of C in pmaports/temp/81voltd, a server-side implementation of the QMI IMS Data service. When the modem asks, 81voltd drives ModemManager to connect a bearer on the ims APN and hands the assigned address back over QMI. scripts/build-81voltd.sh cross-builds it in the same container the other modem-side pieces build in, with qrtr pinned to ae88108 for reproducibility.

Three had every queued message the phone had never received, and the store was written with all of them 101 seconds after the daemon came up, original GSM timestamps intact, plus the network’s own missed-call texts.

Inbound kept working even with the IMS network interface down, because the modem’s own IMS stack rides the PDN context rather than host IP. The modem was IMS-registered on its own, and the host never needed an address for inbound to work; that is why this half hid.

Bringing the IMS bearer up

Outbound was the opposite. ModemManager creates the IMS bearer and configures nothing: the modem assigns it a real IPv6 /64, and the qmapmux0.1 interface sits DOWN with no address. Every outbound submit then timed out after 25 seconds with QMI protocol error (56): 'WmsMessageDeliveryFailure', which reads like a network refusal. Setting Three’s SMSC explicitly changed nothing, which ruled out the missing service centre.

The fix is two lines: ip link set qmapmux0.1 up, then the bearer’s own address on the interface. The same send returns in about a second. fajita-ims-wait does that configuration with verification, reading the address and prefix from the bearer rather than hardcoding them, because the prefix changes between PDN sessions; a recovery came up on a different /64. NetworkManager never touches the interface, and data still flows on qmapmux0.0.

81voltd itself needed verify-and-retry rather than a precondition. The modem asks for its IMS PDN exactly once, when the service appears; if that one connect attempt fails (cm error: no-service), the daemon sits active forever with no bearer, and nothing signals it. No precondition I gated on predicted the failure, and restarting the daemon alone succeeded minutes later. So the unit verifies the outcome instead: ExecStartPost polls for an apn: ims bearer reporting connected and fails the unit otherwise, and Restart=always brings it round again.

Calls connect

With the IMS bearer up, mmcli --voice-create-call plus a start gets through. The journal shows two outbound calls running dialing -> ringing-out -> active -> terminated, with seven and six seconds of active, and one inbound call reaching ringing-in before I missed it. Before 81voltd the same dial terminated in seconds and inbound never paged the device at all.

Audio is wired but does not work, and a mid-call sample now exists. Taken during a clean connected call: profile on Voice Call, both q6voice routing controls on, both VoiceMMode1 substreams open - and the PCM front ends sat PREPARED for the whole call, never RUNNING, with zero q6voice or SLIMBUS events in the kernel log. Every host-side piece is verified correct. The modem completes SIP call control and never starts the ADSP media session. That gap is upstream’s, and a funded postmarketOS q6voice effort from May 2026 targets exactly this codec-to-codec path. 81voltd carries the IMS data bearer, not media.

One systemd trap silently disabled call audio for an hour: PartOf= propagates stop but never start. q6voiced was PartOf=ModemManager.service, so a ModemManager restart took call audio with it and nothing brought it back. The unit now binds both ways.

Two Quickshell apps

The UI is two standalone Quickshell apps, calls.qml and messages.qml, launched as ordinary windows so they tile like any other app and the on-screen keyboard raises over their text fields. Shell helpers do the modem work: fajita-call wraps voice objects, fajita-sms wraps messaging and owns a JSONL store at ~/.local/state/fajita/messages.jsonl, and a polkit rule lets them run from SSH and user services, which have no seat and are otherwise denied. A user service watches ModemManager: incoming call, raise the calls app and notify; incoming SMS, ingest, store, raise the messages app on that conversation. Both apps also have menu entries. The calls app carries a keypad and a recents list: the watcher appends each finished call to a log the list renders newest-first, with direction and duration. That landed after the last deploy window, so it is committed but not yet seen on the phone.

Three bugs from the first days of using them:

Split threads. The network hands back numbers in E.164 (+447700900123) while I dial the national form (07700900123), and storing both verbatim split one conversation in two. The thread held only their side, nothing was right-aligned, and it read like the app had no idea who sent what. The helper now canonicalises on write, a normalize subcommand rewrote the existing store, and the QML mirrors the same function so a dialled number opens the canonical thread. What goes on the wire stays as typed; every send this network has accepted used the dialled form. Alphanumeric senders like Missed Call pass through untouched; an early version squashed one into MissedCall, which then had its own thread.

case "$n" in
  +*)       printf '%s\n' "$n" ;;
  00*)      printf '+%s\n' "${n#00}" ;;
  0[1-9]*)  printf '+%s%s\n' "$CC" "${n#0}" ;;
  *)        printf '%s\n' "$n" ;;
esac

A windowless instance answered IPC. Quickshell outlives the compositor. After a shell restart overnight, the messages app was still running, still owned its IPC socket, and had no window. Its show handler is if (!root.open) root.toggle(), and open was still true from before the restart, so ipc call ... show returned success while hyprctl clients held zero windows. The ipc || spawn launcher shape trusted it, the fallback never ran, and tapping Messages did nothing. fajita-app now verifies the outcome instead of the call: an existing mapped window is focused, otherwise any instance for that QML file is reaped and a fresh one started, waiting until the window is mapped. It forwards IPC arguments too, so raising the messages app on an incoming SMS still lands on that conversation.

The toast did not fit. An SMS notification is Omarchy’s own popup, and its card is sized for a desktop: implicitWidth: Style.space(380), right-anchored, on a 540-px panel. The sender and number hung off the left edge of the screen. The patch script now clamps the card to the panel width; the USB link dropped before the last deploy window, so it has not been seen on the phone either.

What works now

Working: SMS in both directions, on a network where it has no fallback but IMS; the calls and messages apps, with threads merged and notifications raised; calls connecting at the modem with the voice path armed. Not working: audio on a connected call, pinned by the mid-call sample to the modem never starting the ADSP media session. Untouched: Bluetooth, camera.

Mainline Qualcomm hardware ships without the daemons Android used to run, and no one project has them all. The port collects them one at a time from wherever each lives: q6voiced for the voice PCM from postmarketOS, pd-mapper and tqftpserv for the modem from linux-msm, the UCM profiles for audio routing from sdm845-mainline, and 81voltd for IMS from its author.